perf: re-render state readers only when a field they read changes
Follow-up to #7441, which put all substates in one shared store.
Problem
A component that reads one field of a state re-renders when any field of that state changes. A state with 20 fields shown in 20 places re-renders all 20 places when one field changes.
Change
VarData.from_state(state, field)emitsuseStateContext("S", ["field_rx_state_"]). Without a field, the hook has no list, as before._sort_hooksmerges the hooks of a component into oneuseStateContextcall per state, with the union of the fields. A hook without a list reads the whole substate and wins over the field lists.useStateContext(name, fields)keeps the previous snapshot while the listed fields are unchanged, souseSyncExternalStoreskips the render. The store does not change.Var._var_set_stateadds the whole-substate hook, because its JS expression can read any key.
Event handlers in the component read the same object. Their arguments are vars of the component, so their fields are in the list and their closures stay current.
Measured
Own small shop app (8 states, /big page with a 20-field state), prod build, React commit hook counting the function components that render per section. main = 5ae5dcd528.
| Action | main | this PR |
|---|---|---|
| change 1 of 20 shown fields | 20 renders | 1 |
change last_echo (the echo button does not read it) | echo buttons 3 + output 1 | output 1 |
counter inc | counter section 2 | 1 |
| no-op event | 0 | 0 |
Final values are the same on both. No page errors.
Field coverage check on compiled output: for each compiled component, every stateVar.field read is in that component's field list.
- shop app: 34 state hooks, all with a list
examples/playground: 216 state hooks, all with a list, no missing field
Hot reload in dev: adding a field, a text and an event to a mounted component shows the new default value, and both the new and the old fields update on click.
Known limit
A hand-written Var that reads a state field in raw JS, with no VarData.from_state for that field, gets no subscription of its own. Before this PR it was correct only when another var of the same state was in the same component. With this PR, that component re-renders only for the fields in its list, so the hand-written read can show a stale value.