Skip to content

Warn when an unkeyed rx.foreach item has lifecycle events or hooks - #7820

Merged
FarhanAliRaza merged 4 commits into
mainfrom
fix/foreach-lifecycle-without-key-warning
Nov 11, 2026
Merged

FarhanAliRaza merged 4 commits into
mainfrom
fix/foreach-lifecycle-without-key-warning

Conversation

@masenf

@masenf masenf commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Found in the reflex 0.11.0a1 pre-release testing (FINDING-039).

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Description

rx.foreach keys an item without an explicit key by 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. Take rx.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]: removing alpha fires record_unmount("gamma"). key=item fixes 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:

  • MemoizeStatefulPlugin already 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 explicit key=index. The warning names the state fields the iterable reads and the page route (for example over `items` on page `index` ), and suggests key=item.id, or key=item for 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 sets extra={"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.
  • The check costs nothing beyond the scoping the plugin already does, and it runs only during a frontend compile. Evaluating pages (backend workers, reflex db) is untouched. An earlier revision checked every unkeyed item in IterTag.render_component, which made page evaluation about 20% slower on the stateful benchmark page. The current revision times the same as main.
  • docs/library/dynamic-rendering/foreach.md has a new "Keys" section. It explains index keying, shows the on_unmount symptom, mentions per-row state, and says to set key= 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.memo crashes the page). Hooks inside an @rx.memo row 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_warns covers on_mount + on_unmount, a descendant with on_mount, on_unmount alone, add_hooks, an explicit key=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_warn covers key=item, an item without hooks, and an item with only on_click.
  • test_foreach_over_literal_list_warns_only_with_state checks 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_warn checks that evaluating the page without compiling logs nothing.

uv run pytest tests/units passed on the first revision (15643 passed; tests/units/reflex_bench/drivers/test_browser.py was skipped because Chromium is not installed here). ruff check, ruff format and pyright on the changed files are clean. After the follow-up commit, tests/units/compiler/test_memoize_plugin.py passes (166 passed).


Generated by Claude Code

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
@masenf
masenf requested review from a team and Alek99 as code owners November 11, 2026 09:39
@masenf masenf added this to the v0.11.0 milestone Nov 11, 2026
@masenf masenf added the on deck PRs lined up to review / merge next label Nov 11, 2026
@greptile-apps

greptile-apps Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Low impact] The PR appears safe to merge; no new actionable issues were found.

Summary

Adds compile-time warnings for index-keyed rx.foreach rows with lifecycle events or hooks, plus guidance on stable keys.

  • Frontend compilation warns when rx.foreach rows with hooks use index keys.

Reviews (4) · Last reviewed commit: "Name the field and page in the foreach i..." · Reviewed by Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@codspeed

codspeed Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 6.54%

2 warnings

⚠️ 17 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ 1 benchmark with a significant change ran on different hardware

Its base and head results were measured on machines with a different CPU, core count or memory, so the change might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

⚡ 1 improved benchmark
✅ 150 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ test_route_matching[10] 53.6 µs 50.3 µs +6.54%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix/foreach-lifecycle-without-key-warning (4575706) with main (f0088e0)

Open in CodSpeed

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
masenf pushed a commit that referenced this pull request Nov 11, 2026
…runs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
Comment thread reflex/compiler/plugins/memoize.py Resolved
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
masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@masenf

masenf commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

On the CodSpeed reports:

  • First revision (b0910d1): the regression was real. It showed test_evaluate_page[_stateful_page] at −10.45%, from checking every unkeyed item in IterTag.render_component.
  • After the rework (837c682): both test_evaluate_page entries are gone. That revision moved the check into the memoize plugin's loop scoping. 6d7b4eb only moves it within that pass.
  • What remains: test_route_matching[1000] (−5.7%) and test_process_event_burst_independent_tokens[1] (−4.3%), both flagged "ran on different hardware". They show the same drift on every open PR today, and this PR touches neither path.

Generated by Claude Code

masenf pushed a commit that referenced this pull request Nov 11, 2026
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.
masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@FarhanAliRaza
FarhanAliRaza merged commit cc9727f into main Nov 11, 2026
168 checks passed
@FarhanAliRaza
FarhanAliRaza deleted the fix/foreach-lifecycle-without-key-warning branch November 11, 2026 16:02
masenf added a commit that referenced this pull request Nov 11, 2026
…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)
masenf pushed a commit that referenced this pull request Nov 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dxjadq3qkhKnqZR2b9WBFi
@masenf masenf added the release-train Issues found during agentic /prerelease-testing fanout, not necessarily validated bugs. label Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

on deck PRs lined up to review / merge next release-train Issues found during agentic /prerelease-testing fanout, not necessarily validated bugs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants