Run the hooks of an rx.foreach item inside the loop body
Fixes #7791
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
- Bug fix (non-breaking change which fixes an issue)
Problem
A hook of a component inside rx.foreach was hoisted to the top of the Foreach memo body, outside the loop:
rx.foreach(S.items, lambda x: rx.text(x, on_mount=S.seen(x)))
const Foreach_comp_... = memo(({children}) => {
useEffect(() => { ...seen({ x: x_rx_state_ }) }, []); // x_rx_state_ does not exist here
return items.map((x_rx_state_, index) => jsx(Text, {key: index}, x_rx_state_))
});
- A hook that read the loop variable crashed the page:
ReferenceError: x_rx_state_ is not defined. - A hook that did not read it ran once for the whole list, not once per item.
This applied to on_mount, on_unmount, and hooks from add_hooks.
Fix
When an item has such hooks, Foreach compiles the item to a component at module level, in the same generated file, and passes the loop variables as props:
const ForeachItem_<hash> = (({x_rx_state_, index}) => {
useEffect(() => { ...seen({ x: x_rx_state_ }) }, []);
return (jsx(Text, {key: index}, x_rx_state_))
});
items.map((x_rx_state_, index) => jsx(ForeachItem_<hash>, {key: index, x_rx_state_, index}))
The hooks belong to one item, and the loop variables are in scope. The item component also receives the variables that the enclosing generated code binds: the loop variables of enclosing rx.foreach loops and the parameters of an enclosing @rx.memo function. The hooks that the item key needs stay in the outer scope, because the loop body evaluates the key there.
An item gets its own component only when:
- a component in it has a lifecycle event,
_get_hooks, oradd_hooks, or - one of its hooks reads the loop variable or the index variable.
All other loops compile to the same code as before.
The item component is a normal top-level function component, so React Compiler accepts it. A first version called the hooks in a function expression inside .map(); React Compiler rejects that ("Hooks must be called at the top level").
Testing
- New unit tests in
tests/units/compiler/test_memoize_plugin.py:on_mount,add_hooksand index-variable items, a nested loop that reads the outer loop variable, a state-backed item key, a lifecycle event in a prop component, a loop inside a memo function, and an item without such hooks. tests/units/compilerandtests/units/components/core: 520 passed. All unit tests that matchmemo or foreach: 434 passed.- Live check,
reflex run --env prod, Chromium, default config:- Each item fires its own
on_mount, also in a nested loop. - A new item fires only its own
on_mount. A removed item fires itson_unmount. - An unrelated state update fires nothing, and an input inside an item keeps its text.
- No console errors.
- Each item fires its own
- React Compiler (the experimental release and plugin settings from #7694) on the generated memo module: the item components and the
Foreachcomponents compile, with no hook errors.
Known limits
- The item component receives loop variables and memo parameters. A value that a raw hook string declares in the enclosing function, and that the item reads without that hook in its
VarData, is not in scope in the item component.