Render state contexts without a component per substate on the server

Merged
AD
1mo ago
+201-27

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)

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?

What and why

#6181 gave every substate its own SubstateProvider so a delta re-renders only the contexts it changes. On the server that component has no job (the page renders once and no delta is ever applied), but it adds a second render level per substate. React's server renderer recurses once per level, so the depth of every page render doubled.

In an app with ~150 substates this intermittently failed every route in the dev server with a 500 (Unexpected Server Error / SyntaxError: Invalid regular expression: /[^a-z-]/: Stack overflow) that cleared on its own after 40–60 s. Measured on that app:

  • Before V8 has optimised React's render functions (after a restart, and again after a major GC), the render reaches the first Emotion css element 1,254 frames deep with about 7 KB of the default ~984 KB stack left.
  • React Fizz recovers from overflows whose message is exactly Maximum call stack size exceeded: it re-renders that subtree from a fresh stack. An overflow that surfaces as anything else is not recovered, and the request fails. The regex SyntaxError above is one example; a TypeError reading $$typeof on a useContext is another.
  • Running the same app with the stack only 1–2.5% smaller (--stack-size=971 and 960) reproduced the 500s reliably, including the fail-then-recover pattern.

Change

context_template now emits the substates once as a SUBSTATES table, plus two providers chosen at module load:

  • ClientStateProvider (browser): unchanged behaviour. DispatchProvider and one SubstateProvider per substate, memoized on children, built with a loop instead of a generated nested expression.
  • ServerStateProvider (typeof document === "undefined", the same test useIsomorphicLayoutEffect uses): bare context providers carrying initialState, one level per substate. No reducers or dispatch registry, because nothing dispatches on the server.

Hydration is unaffected: wrapper components produce no DOM and do not fork the tree. In the app above there were no hydration warnings on any page, with the same console output as before the change.

Same app, with the fix:

BeforeAfter
Depth of first css element1,254 frames646 frames
Stack free on a cold render~0.7%~45%
--stack-size 950–990 KB and 960/971500s at 960 and 971all clean
--stack-size down to 500 KBnot testedall clean

Tests

  • test_context_template_server_state_provider_is_flat (new) and the updated test_context_template_one_provider_per_substate fail on main and pass here.
  • test_context_js_template.py located EventLoopProvider by the old export function StateProvider text; it now anchors on the next declaration.
  • tests/units/compiler: 308 passed. The full unit suite has the same failures with and without this change (227 reflex_cli tests that fail in my environment on clean main too).
  • tests/integration/tests_playwright/test_router_query.py: 8 passed in dev and prod, including #6181's case where one delta updates two sibling substates.
  • pre-commit hooks pass on the changed files.

πŸ€– Generated with Claude Code

https://claude.ai/code/session_01CQ4shGzkLwvScpUv6vxqHm


Generated by Claude Code