Repository navigation
Warn when an unkeyed rx.foreach item has lifecycle events or hooks - #7820
Conversation
rx.foreach keys an item without an explicit key by its index. Lifecycle events and hooks of the item then follow the position, not the item: after the first of three items is removed, on_unmount fires for the last one, with its payload. key=item cannot be the default (object items stringify to duplicate keys, and duplicate primitives collide), so a frontend compile now warns once (deduplicated) when an unkeyed item, or a descendant, has on_mount / on_unmount or hooks of its own, using the memoize plugin's instance-hook check. Processes that only evaluate pages (REFLEX_SKIP_COMPILE, such as prod backend workers and reflex db) do not warn. The foreach docs now explain index keying and when to set key=. Found in the reflex 0.11.0a1 pre-release testing (FINDING-039). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
[Low impact] The PR appears safe to merge; no new actionable issues were found. SummaryAdds compile-time warnings for index-keyed
Reviews (4) · Last reviewed commit: "Name the field and page in the foreach i..." · Reviewed by Greptile |
There was a problem hiding this comment.
All reported issues were addressed across 5 files
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
Merging this PR will improve performance by 6.54%2 warnings
Performance Changes
Tip Curious why performance improved? Comment Comparing |
Checking every unkeyed item's subtree when rx.foreach renders it made page evaluation about 20% slower on the stateful benchmark page. The memoize plugin already finds the foreach items that have hooks of their own when it scopes them into loop wrappers, so it now warns there, at no extra cost and only during a frontend compile. An explicit key=index warns too, and the message only suggests key=item for a list of unique strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
…runs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
A nested Foreach has no tag, so the plugin scopes its items without building a wrapper for it; the warning now runs wherever a foreach's item subtree is scoped, so an inner loop inside a keyed outer row warns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
|
On the CodSpeed reports:
Generated by Claude Code |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
…nt lists The warning printed the compiled JS name of the iterable. It now names the state fields the iterable reads and the page route. A constant literal list never reorders, so its index keys are stable and it no longer warns. A literal list that holds a state var still warns. Dedupe only collapses identical warnings within one process: each hot reload starts a new worker, so the warning repeats there. Fix the test comment and docstring that said otherwise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
…7820) * Warn when an unkeyed rx.foreach item has lifecycle events or hooks rx.foreach keys an item without an explicit key by its index. Lifecycle events and hooks of the item then follow the position, not the item: after the first of three items is removed, on_unmount fires for the last one, with its payload. key=item cannot be the default (object items stringify to duplicate keys, and duplicate primitives collide), so a frontend compile now warns once (deduplicated) when an unkeyed item, or a descendant, has on_mount / on_unmount or hooks of its own, using the memoize plugin's instance-hook check. Processes that only evaluate pages (REFLEX_SKIP_COMPILE, such as prod backend workers and reflex db) do not warn. The foreach docs now explain index keying and when to set key=. Found in the reflex 0.11.0a1 pre-release testing (FINDING-039). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi * Warn about index-keyed foreach items from the memoize plugin Checking every unkeyed item's subtree when rx.foreach renders it made page evaluation about 20% slower on the stateful benchmark page. The memoize plugin already finds the foreach items that have hooks of their own when it scopes them into loop wrappers, so it now warns there, at no extra cost and only during a frontend compile. An explicit key=index warns too, and the message only suggests key=item for a list of unique strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi * Warn about index-keyed items of nested foreach loops too A nested Foreach has no tag, so the plugin scopes its items without building a wrapper for it; the warning now runs wherever a foreach's item subtree is scoped, so an inner loop inside a keyed outer row warns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi * Name the field and page in the foreach index-key warning, skip constant lists The warning printed the compiled JS name of the iterable. It now names the state fields the iterable reads and the page route. A constant literal list never reorders, so its index keys are stable and it no longer warns. A literal list that holds a state var still warns. Dedupe only collapses identical warnings within one process: each hot reload starts a new worker, so the warning repeats there. Fix the test comment and docstring that said otherwise. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Farhan <www.mfarvirus@gmail.com> (cherry picked from commit cc9727f)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
Found in the reflex 0.11.0a1 pre-release testing (FINDING-039).
All Submissions:
Type of change
New Feature Submission:
Changes To Core Features:
Description
rx.foreachkeys an item without an explicitkeyby its index (IterTag.render_component). Since #7793, each item runs its own hooks, but with index keys those hooks follow the position, not the item. Takerx.foreach(S.items, lambda item: rx.text(item, on_mount=S.record_mount(item), on_unmount=S.record_unmount(item)))with items[alpha, beta, gamma]: removingalphafiresrecord_unmount("gamma").key=itemfixes it for a list of unique strings. It is not a safe default, because object items stringify to duplicate keys and duplicate primitives collide.This PR adds a warning and docs:
MemoizeStatefulPluginalready finds each foreach item subtree with hooks of one rendered instance (lifecycle events,_get_hooks,add_hooks, components held in props), because it gives such items their own wrapper inside the loop (Pass loop variables to auto-memo wrappers inside rx.foreach #7793). When that scoping changes a Foreach and the item's key is the loop index, the plugin logs a warning. That covers both a missing key and an explicitkey=index. The warning names the state fields the iterable reads and the page route (for exampleover `items` on page `index`), and suggestskey=item.id, orkey=itemfor a list of unique strings. A constant literal list never reorders, so it does not warn; a literal list that holds a state var does. The record setsextra={"dedupe": True}, which collapses identical warnings within one process. Each hot reload starts a new worker, so the warning repeats once per reload until the key is fixed.reflex db) is untouched. An earlier revision checked every unkeyed item inIterTag.render_component, which made page evaluation about 20% slower on the stateful benchmark page. The current revision times the same asmain.docs/library/dynamic-rendering/foreach.mdhas a new "Keys" section. It explains index keying, shows theon_unmountsymptom, mentions per-row state, and says to setkey=to a unique, stable value.Related but out of scope: FINDING-040 (hoisted lifecycle hooks of a cond inside
@rx.memo) and FINDING-041 (a foreach with item hooks inside@rx.memocrashes the page). Hooks inside an@rx.memorow are not scoped per item yet, so they don't warn yet either.Tests
In
tests/units/compiler/test_memoize_plugin.py, each test compiles a page:test_foreach_item_with_hooks_keyed_by_index_warnscoverson_mount+on_unmount, a descendant withon_mount,on_unmountalone,add_hooks, an explicitkey=index, and an inner loop of a keyed row. Each warns once, names the field and page, and the record opts into deduplication.test_foreach_item_keyed_or_without_hooks_does_not_warncoverskey=item, an item without hooks, and an item with onlyon_click.test_foreach_over_literal_list_warns_only_with_statechecks that a constant list does not warn and a literal list holding a state var does.test_evaluating_a_foreach_item_with_hooks_does_not_warnchecks that evaluating the page without compiling logs nothing.uv run pytest tests/unitspassed on the first revision (15643 passed;tests/units/reflex_bench/drivers/test_browser.pywas skipped because Chromium is not installed here).ruff check,ruff formatandpyrighton the changed files are clean. After the follow-up commit,tests/units/compiler/test_memoize_plugin.pypasses (166 passed).Generated by Claude Code