Create separate context providing components for each state#6181
Create separate context providing components for each state#6181
Conversation
Instead of combining all states and dispatchers into the same component (which is the re-render domain), separate each state into its own provider component and combine those into StatesProvider. Since the StatesProvider doesn't directly depend on each state's context, it doesn't have to re-render just because some substate changed.
Greptile SummaryThis PR refactors the React state management in the generated Key changes:
Potential issues found:
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
SP["StatesProvider"]
DP["DispatchProvider\n(useRef mutable dispatch registry)"]
SP1["StateProvider (state 1)\nuseReducer + useEffect to register dispatcher"]
SP2["StateProvider (state 2)\nuseReducer + useEffect to register dispatcher"]
ELP["EventLoopProvider\nuseContext(DispatchContext) → dispatch ref"]
AW["AppWrap (children)"]
WS["WebSocket (state.js)\ndispatch[substate](delta)"]
SP --> DP
DP --> SP1
SP1 --> SP2
SP2 --> ELP
ELP --> AW
ELP -.->|"passes dispatch ref"| WS
SP1 -.->|"registers dispatcher via useEffect"| DP
SP2 -.->|"registers dispatcher via useEffect"| DP
WS -.->|"calls dispatch[substate](...)"| DP
Last reviewed commit: b78ebc6 |
| const DispatchProvider = ({{ children }}) => {{ | ||
| const dispatchers = useRef({{}}); | ||
| return useMemo(() => createElement(DispatchContext, {{ value: dispatchers }}, children), [children, dispatchers]); |
There was a problem hiding this comment.
Ref object used as plain dictionary
DispatchProvider passes the useRef({}) return value (a ref object with shape { current: {} }) as the context value. Then in StateProvider (line 404), dispatchers are set directly on this ref object (dispatchers[state_name] = dispatch_state) rather than on dispatchers.current. Similarly, EventLoopProvider and state.js access dispatch[substate] directly.
This works because the ref object is a stable mutable JS object, but it's unconventional — the .current property initialized to {} goes unused, and properties are set directly on the ref itself. A plain useRef(null) or useMemo(() => ({}), []) would make the intent clearer. Alternatively, if .current is meant to be the container, both the reads and writes should go through .current:
const DispatchProvider = ({ children }) => {
const dispatchers = useRef({});
return useMemo(() => createElement(DispatchContext, { value: dispatchers.current }, children), [children, dispatchers]);
}
…and update StateProvider to match (dispatchers.current[state_name] = ...).
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| const StateProvider = ({{ children, state_name, ctx_name }}) => {{ | ||
| const dispatchers = useContext(DispatchContext); | ||
| const [state, dispatch_state] = useReducer(applyDelta, initialState[state_name]); | ||
| useEffect(() => {{ | ||
| dispatchers[state_name] = dispatch_state; | ||
| return () => delete dispatchers[state_name]; | ||
| }}, [dispatchers, dispatch_state, state_name]); | ||
| return useMemo( | ||
| () => | ||
| createElement( | ||
| StateContexts[ctx_name], | ||
| {{ value: state }}, | ||
| children, | ||
| ), | ||
| [children, state, ctx_name], | ||
| ); | ||
| }} |
There was a problem hiding this comment.
Dispatcher registration via useEffect creates a timing gap
Each StateProvider registers its dispatcher in a useEffect (line 403-405). Since useEffect runs after paint and React commits effects bottom-up (children first), the EventLoopProvider's socket-connecting effect (in state.js line 997-1011) fires before the StateProvider effects that register dispatchers.
In practice, the websocket connection is async so dispatchers are likely registered by the time the first "event" message arrives. However, if the socket connects very quickly (e.g. reconnect scenario), state.js line 681 (dispatch[substate](...)) could throw a TypeError because the dispatcher for that substate isn't registered yet on the ref object.
Consider using useLayoutEffect instead of useEffect for the dispatcher registration to ensure dispatchers are available synchronously after render, before any child effects or event handlers run.
Instead of combining all states and dispatchers into the same component (which is the re-render domain), separate each state into its own provider component and combine those into StatesProvider. Since the StatesProvider doesn't directly depend on each state's context, it doesn't have to re-render just because some substate changed.