Render state contexts without a component per substate on the server
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
csselement 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 regexSyntaxErrorabove is one example; aTypeErrorreading$$typeofon auseContextis another. - Running the same app with the stack only 1β2.5% smaller (
--stack-size=971and960) 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.DispatchProviderand oneSubstateProviderper substate, memoized onchildren, built with a loop instead of a generated nested expression.ServerStateProvider(typeof document === "undefined", the same testuseIsomorphicLayoutEffectuses): bare context providers carryinginitialState, 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:
| Before | After | |
|---|---|---|
Depth of first css element | 1,254 frames | 646 frames |
| Stack free on a cold render | ~0.7% | ~45% |
--stack-size 950β990 KB and 960/971 | 500s at 960 and 971 | all clean |
--stack-size down to 500 KB | not tested | all clean |
Tests
test_context_template_server_state_provider_is_flat(new) and the updatedtest_context_template_one_provider_per_substatefail onmainand pass here.test_context_js_template.pylocatedEventLoopProviderby the oldexport function StateProvidertext; 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 (227reflex_clitests that fail in my environment on cleanmaintoo).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