perf: re-render state readers only when a field they read changes

FA
3d ago
+206-12

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) emits useStateContext("S", ["field_rx_state_"]). Without a field, the hook has no list, as before.
  • _sort_hooks merges the hooks of a component into one useStateContext call 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, so useSyncExternalStore skips the render. The store does not change.
  • Var._var_set_state adds 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.

Actionmainthis PR
change 1 of 20 shown fields20 renders1
change last_echo (the echo button does not read it)echo buttons 3 + output 1output 1
counter inccounter section 21
no-op event00

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.