Repository navigation
Conversation
Each rendered item publishes its value and index through a ScopedValues provider, and loop vars read them from that scope, so event handlers hoisted into useCallback and descendants compiled into their own memo modules can use them. Every item renders through a component of its own, so an unnamed rx.client_state var in the item function is per item, and an explicit key on the item keys the list. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DE4XVgDNtq2i89CMVspJ9
[Medium impact] The PR appears safe to merge, with two non-blocking documentation corrections still outstanding. FindingsSummaryThis PR gives each
Reviews (5) · Last reviewed commit: "Refresh the memo.pyi hash for this branc..." · Reviewed by Greptile |
| item to key by identity instead: | ||
|
|
||
| ```python | ||
| rx.foreach(TodoState.items, lambda item: todo_row(item, key=item)) |
There was a problem hiding this comment.
| If you want a row's state to track a var while staying editable in between, seed | ||
| it and then push updates explicitly with `on_mount=count.set(index)`, or on a | ||
| `rx.fragment(key=..., on_mount=...)` to tie the reset to something of your own | ||
| choosing. |
There was a problem hiding this comment.
on_mount=count.set(index) does not make the value follow later changes to index. With positional keys, the existing row stays mounted, so this event never runs again. Show an update that runs when the source changes, or explain that on_mount only resets the value when the row mounts. The same advice in docs/wrapping-react/overview.md needs the same correction.
There was a problem hiding this comment.
8 issues found across 19 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="docs/wrapping-react/overview.md">
<violation number="1" location="docs/wrapping-react/overview.md:193">
P2: `on_mount=count.set(index)` does not make the state follow later item changes: `on_mount` runs only once per mounted row, while positional `rx.foreach` rows stay mounted. Recommend an event/update tied to the changed item, or key the row so replacement remounts it.</violation>
</file>
<file name="news/+foreach-scoped-loop-vars.bugfix.md">
<violation number="1" location="news/+foreach-scoped-loop-vars.bugfix.md:1">
P2: This release note overstates the per-item state guarantee: `foreach` bodies inside an explicit `@rx.memo` are not split, so an unnamed client-state var in that item function remains shared. Qualify the note with this exception or point users to the required memoized item function.</violation>
</file>
<file name="tests/units/compiler/test_memoize_plugin.py">
<violation number="1" location="tests/units/compiler/test_memoize_plugin.py:2969">
P2: `handler is not foreach_block` compares object identity, not block contents; each separate `split()` call creates fresh strings, so this passes even if both blocks are emitted by one export. Compare the blocks by value or assert their export names differ.</violation>
</file>
<file name="docs/library/dynamic-rendering/foreach.md">
<violation number="1" location="docs/library/dynamic-rendering/foreach.md:317">
P2: This example cannot run: `row` is undefined here, and the preceding `todo_row` requires `index` and does not accept `key`. Put `key=item` on the component returned by the render lambda (or update the row helper) instead.</violation>
<violation number="2" location="docs/library/dynamic-rendering/foreach.md:321">
P2: Clarify that `on_mount=count.set(index)` only resets the value when the row mounts; it does not follow later index changes under positional keys. Make the same correction in `docs/wrapping-react/overview.md`.</violation>
<violation number="3" location="docs/library/dynamic-rendering/foreach.md:341">
P2: This identity-key example raises `TypeError` because `todo_row` does not accept `key` and the callback does not provide its required `index`. Put the key on a component returned by the callback, for example `rx.fragment(todo_row(item, index), key=item)`; apply the same pattern to the earlier `row(item, key=item)` example.</violation>
</file>
<file name="packages/reflex-base/src/reflex_base/.templates/web/utils/client_state.js">
<violation number="1" location="packages/reflex-base/src/reflex_base/.templates/web/utils/client_state.js:313">
P2: `ScopedValues` creates a new context value on every render, so memoized item consumers re-render whenever the loop remaps, even when their item and index are unchanged. Preserve the chained value when `parent` and each provided entry are `Object.is`-equal.</violation>
</file>
<file name="packages/reflex-base/news/+foreach-scoped-loop-vars.bugfix.md">
<violation number="1" location="packages/reflex-base/news/+foreach-scoped-loop-vars.bugfix.md:1">
P2: This release note overstates the guarantee: `foreach` inside an explicit `@rx.memo` body still shares unnamed client state across items. Mention this exception so users do not rely on per-item state in the unsupported case.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| seed when the list's contents change, because `rx.foreach` keys rows by position and | ||
| a row that does not unmount never re-claims its slot. The item itself still follows | ||
| the data -- only the seeded state lags. Pass `key=` on the item to tie a row's state | ||
| to the item instead, or set the value explicitly (`on_mount=count.set(index)`) when |
There was a problem hiding this comment.
P2: on_mount=count.set(index) does not make the state follow later item changes: on_mount runs only once per mounted row, while positional rx.foreach rows stay mounted. Recommend an event/update tied to the changed item, or key the row so replacement remounts it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/wrapping-react/overview.md, line 193:
<comment>`on_mount=count.set(index)` does not make the state follow later item changes: `on_mount` runs only once per mounted row, while positional `rx.foreach` rows stay mounted. Recommend an event/update tied to the changed item, or key the row so replacement remounts it.</comment>
<file context>
@@ -172,19 +172,28 @@ def counter():
+seed when the list's contents change, because `rx.foreach` keys rows by position and
+a row that does not unmount never re-claims its slot. The item itself still follows
+the data -- only the seeded state lags. Pass `key=` on the item to tie a row's state
+to the item instead, or set the value explicitly (`on_mount=count.set(index)`) when
+you want it to follow. See
+[foreach](/docs/library/dynamic-rendering/foreach) for the worked example.
</file context>
| to the item instead, or set the value explicitly (`on_mount=count.set(index)`) when | |
| to the item instead, or update it from an event when |
| @@ -0,0 +1 @@ | |||
| `rx.foreach` item and index values are now available to event handlers and memoized components rendered inside the loop, and each item renders as a component of its own, so an unnamed `rx.client_state` var declared in the item function is per item. | |||
There was a problem hiding this comment.
P2: This release note overstates the per-item state guarantee: foreach bodies inside an explicit @rx.memo are not split, so an unnamed client-state var in that item function remains shared. Qualify the note with this exception or point users to the required memoized item function.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At news/+foreach-scoped-loop-vars.bugfix.md, line 1:
<comment>This release note overstates the per-item state guarantee: `foreach` bodies inside an explicit `@rx.memo` are not split, so an unnamed client-state var in that item function remains shared. Qualify the note with this exception or point users to the required memoized item function.</comment>
<file context>
@@ -0,0 +1 @@
+`rx.foreach` item and index values are now available to event handlers and memoized components rendered inside the loop, and each item renders as a component of its own, so an unnamed `rx.client_state` var declared in the item function is per item.
</file context>
| `rx.foreach` item and index values are now available to event handlers and memoized components rendered inside the loop, and each item renders as a component of its own, so an unnamed `rx.client_state` var declared in the item function is per item. | |
| `rx.foreach` item and index values are now available to event handlers and memoized components rendered inside the loop. Each item function renders as its own component, so an unnamed `rx.client_state` var declared there is per item; loops inside an explicit `@rx.memo` still require a memoized item function for per-item state. |
| # The read has to happen below the provider, which means in a module of its | ||
| # own: hooks are hoisted to the top of whichever component they land in, so | ||
| # a read in the module that *renders* the provider would sit above it. | ||
| assert handler is not foreach_block, "the handler must be its own memo module" |
There was a problem hiding this comment.
P2: handler is not foreach_block compares object identity, not block contents; each separate split() call creates fresh strings, so this passes even if both blocks are emitted by one export. Compare the blocks by value or assert their export names differ.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/units/compiler/test_memoize_plugin.py, line 2969:
<comment>`handler is not foreach_block` compares object identity, not block contents; each separate `split()` call creates fresh strings, so this passes even if both blocks are emitted by one export. Compare the blocks by value or assert their export names differ.</comment>
<file context>
@@ -2815,6 +2918,135 @@ def page() -> Component:
+ # The read has to happen below the provider, which means in a module of its
+ # own: hooks are hoisted to the top of whichever component they land in, so
+ # a read in the module that *renders* the provider would sit above it.
+ assert handler is not foreach_block, "the handler must be its own memo module"
+ # Inside the loop body the callback parameter of the same name shadows any
+ # hoisted read, so inline uses see the real per-item value.
</file context>
| assert handler is not foreach_block, "the handler must be its own memo module" | |
| assert handler != foreach_block, "the handler must be its own memo module" |
| item to key by identity instead: | ||
|
|
||
| ```python | ||
| rx.foreach(TodoState.items, lambda item: todo_row(item, key=item)) |
There was a problem hiding this comment.
P2: This identity-key example raises TypeError because todo_row does not accept key and the callback does not provide its required index. Put the key on a component returned by the callback, for example rx.fragment(todo_row(item, index), key=item); apply the same pattern to the earlier row(item, key=item) example.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/library/dynamic-rendering/foreach.md, line 341:
<comment>This identity-key example raises `TypeError` because `todo_row` does not accept `key` and the callback does not provide its required `index`. Put the key on a component returned by the callback, for example `rx.fragment(todo_row(item, index), key=item)`; apply the same pattern to the earlier `row(item, key=item)` example.</comment>
<file context>
@@ -220,8 +220,146 @@ def foreach_complex_dict_example():
+item to key by identity instead:
+
+```python
+rx.foreach(TodoState.items, lambda item: todo_row(item, key=item))
+```
+
</file context>
| rx.foreach(TodoState.items, lambda item: todo_row(item, key=item)) | |
| rx.foreach(TodoState.items, lambda item, index: rx.fragment(todo_row(item, index), key=item)) |
| const parent = useContext(ScopedValuesContext); | ||
| // A fresh object each render is correct here -- the values themselves change | ||
| // per render, and nothing subscribes to them. | ||
| const chained = { parent, values }; |
There was a problem hiding this comment.
P2: ScopedValues creates a new context value on every render, so memoized item consumers re-render whenever the loop remaps, even when their item and index are unchanged. Preserve the chained value when parent and each provided entry are Object.is-equal.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/reflex-base/src/reflex_base/.templates/web/utils/client_state.js, line 313:
<comment>`ScopedValues` creates a new context value on every render, so memoized item consumers re-render whenever the loop remaps, even when their item and index are unchanged. Preserve the chained value when `parent` and each provided entry are `Object.is`-equal.</comment>
<file context>
@@ -280,6 +280,62 @@ export const withClientStateScope = (Component) => {
+ const parent = useContext(ScopedValuesContext);
+ // A fresh object each render is correct here -- the values themselves change
+ // per render, and nothing subscribes to them.
+ const chained = { parent, values };
+ return createElement(
+ ScopedValuesContext.Provider,
</file context>
| @@ -0,0 +1 @@ | |||
| `rx.foreach` item and index values are now available to event handlers and memoized components rendered inside the loop, and each item renders as a component of its own, so an unnamed `rx.client_state` var declared in the item function is per item. | |||
There was a problem hiding this comment.
P2: This release note overstates the guarantee: foreach inside an explicit @rx.memo body still shares unnamed client state across items. Mention this exception so users do not rely on per-item state in the unsupported case.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/reflex-base/news/+foreach-scoped-loop-vars.bugfix.md, line 1:
<comment>This release note overstates the guarantee: `foreach` inside an explicit `@rx.memo` body still shares unnamed client state across items. Mention this exception so users do not rely on per-item state in the unsupported case.</comment>
<file context>
@@ -0,0 +1 @@
+`rx.foreach` item and index values are now available to event handlers and memoized components rendered inside the loop, and each item renders as a component of its own, so an unnamed `rx.client_state` var declared in the item function is per item.
</file context>
| `rx.foreach` item and index values are now available to event handlers and memoized components rendered inside the loop, and each item renders as a component of its own, so an unnamed `rx.client_state` var declared in the item function is per item. | |
| `rx.foreach` item and index values are now available to event handlers and memoized components rendered inside the loop. Automatically split items render as separate components, so an unnamed `rx.client_state` var declared in the item function is per item; loops inside an explicit `@rx.memo` still require a separately memoized item function for per-item state. |
| unmounts the old rows and the new ones seed themselves: | ||
|
|
||
| ```python | ||
| rx.foreach(State.items, lambda item: row(item, key=item)) |
There was a problem hiding this comment.
P2: This example cannot run: row is undefined here, and the preceding todo_row requires index and does not accept key. Put key=item on the component returned by the render lambda (or update the row helper) instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/library/dynamic-rendering/foreach.md, line 317:
<comment>This example cannot run: `row` is undefined here, and the preceding `todo_row` requires `index` and does not accept `key`. Put `key=item` on the component returned by the render lambda (or update the row helper) instead.</comment>
<file context>
@@ -220,8 +220,146 @@ def foreach_complex_dict_example():
+unmounts the old rows and the new ones seed themselves:
+
+```python
+rx.foreach(State.items, lambda item: row(item, key=item))
+```
+
</file context>
| rx.foreach(State.items, lambda item: row(item, key=item)) | |
| rx.foreach(State.items, lambda item: rx.text(rx.client_state(item).value, key=item)) |
| ``` | ||
|
|
||
| If you want a row's state to track a var while staying editable in between, seed | ||
| it and then push updates explicitly with `on_mount=count.set(index)`, or on a |
There was a problem hiding this comment.
P2: Clarify that on_mount=count.set(index) only resets the value when the row mounts; it does not follow later index changes under positional keys. Make the same correction in docs/wrapping-react/overview.md.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/library/dynamic-rendering/foreach.md, line 321:
<comment>Clarify that `on_mount=count.set(index)` only resets the value when the row mounts; it does not follow later index changes under positional keys. Make the same correction in `docs/wrapping-react/overview.md`.</comment>
<file context>
@@ -220,8 +220,146 @@ def foreach_complex_dict_example():
+```
+
+If you want a row's state to track a var while staying editable in between, seed
+it and then push updates explicitly with `on_mount=count.set(index)`, or on a
+`rx.fragment(key=..., on_mount=...)` to tie the reset to something of your own
+choosing.
</file context>
Merging this PR will degrade performance by 11.33%2 warnings
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
…ude/foreach-scoped-loop-vars
…ude/foreach-scoped-loop-vars
…ude/foreach-scoped-loop-vars
…ude/foreach-scoped-loop-vars # Conflicts: # tests/js/client_state.test.js
The merge commit kept the conflict markers in tests/js/client_state.test.js. Keep the scoped-value lifetime tests and drop the registry prop the provider no longer takes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DE4XVgDNtq2i89CMVspJ9
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DE4XVgDNtq2i89CMVspJ9
All Submissions:
Type of change
Description
Follow-up to #6936, which this PR is stacked on. It carries the
rx.foreachloop-var scoping that was split out of that PR.What changes for users
rx.foreachare available anywhere in the item's subtree, including event handlers the compiler hoists intouseCallbackand descendants it compiles into their own memo modules. Fixes [REF-2745] rx.form handle_submit lambda cannot reference foreach vars #3210.rx.client_statevar declared in the item function is per item, and it can be seeded from the item or index.key=on the item is used to key the list; otherwise it is keyed by position as before.@rx.memobody a loop is not split up, so per-item client state there still uses an@rx.memoitem function. The foreach docs say so.Implementation
IterTagbuilds loop vars withscoped_loop_var, which reads the value from an enclosingScopedValuesprovider throughuseScopedValue; the map callback's parameter shadows the hook inside the loop body itself.Foreachrenders each item insideScopedValues, which also opens a client state scope, and no longer seals its subtree from auto-memoization.MemoizeStatefulPluginwraps each loop item in a snapshot memo component after a sealed sub-walk over the item, and a forced snapshot body is hashed by its whole subtree.Changes To Core Features:
Generated by Claude Code