From ddad5c353c6708d9fb97a9ecb56ca6ebe748c21a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 23:13:31 -0400 Subject: [PATCH] feat(ssr): run hooks during server rendering (v3.5.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ui.RenderToString was hook-less — a component calling any hook errored ("called outside component context"), so only hook-free trees could be server-rendered. RenderToString now installs a transient hook fiber per component (withSSRHookFiber): GoUseState returns its initial value, GoUseRef/ GoUseMemo compute, GoUseContextValue resolves to the nearest provider value, and GoUseEffect is queued but never committed (so effects don't run on the server) — matching React's ReactDOMServerIntegrationHooks. Context propagation: the SSR renderer threads an inherited context map (descriptor ID -> value) through the recursion, derived at each ContextProvider boundary via deriveContextValues, so context flows through host elements and nested providers override correctly. Scope/limits: hook-component SSR uses the package-global current fiber (like the client reconciler), so a process renders hook trees one at a time. The streaming SSR path runs hooks but passes nil context (context threading there is a follow-up); the buffered RenderToString path is fully supported. Replaces the old boundary test with ui/ssr_hooks_test.go (useState/useRef/ useMemo/useContext + effect-skip + nested-provider). Full suite passes, native + wasm build clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 28 +++++ docs/REACT_TEST_PARITY.md | 2 +- .../assets/docs/latest-release-notes.md | 25 ++-- .../helper_branches_additional_test.go | 8 +- internal/runtime/hydration_helper_gap_test.go | 16 +-- internal/runtime/ssr.go | 111 +++++++++++------- internal/runtime/ssr_stream.go | 6 +- ui/ssr_hooks_boundary_test.go | 60 ---------- ui/ssr_hooks_test.go | 108 +++++++++++++++++ 9 files changed, 233 insertions(+), 131 deletions(-) delete mode 100644 ui/ssr_hooks_boundary_test.go create mode 100644 ui/ssr_hooks_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f07038d..f77149a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## v3.5.0 - 2026-06-24 + +### Added + +- **Hooks now run during server rendering (`ui.RenderToString`).** Previously the + string serializer was hook-less: a component calling any hook errored with + "called outside component context", so only hook-free trees could be + server-rendered. `RenderToString` now installs a transient hook fiber per + component, so on the server: + - `GoUseState` returns its initial value (the setter is a no-op); + - `GoUseRef` and `GoUseMemo` compute normally; + - `GoUseContextValue` resolves to the nearest provider's value — context flows + down through host elements and nested providers override correctly; + - `GoUseEffect` is queued but never committed, so effects do not run on the + server (matching React). + + This makes GWC able to server-render real (hook-using) components, matching + React's `ReactDOMServerIntegrationHooks`. Implemented by threading the inherited + context map through the SSR renderer and deriving it at each `ContextProvider` + boundary. Covered by `ui/ssr_hooks_test.go`. + + Notes: SSR-with-hooks uses the package-global current-fiber like the client + reconciler, so a single process renders hook components one tree at a time + (concurrent goroutine SSR of hook components needs external serialization — a + pre-existing hook-architecture constraint). The streaming SSR path runs hooks + but does not yet thread context (a documented follow-up); the buffered + `RenderToString` path is fully supported. + ## v3.4.10 - 2026-06-24 ### Fixed diff --git a/docs/REACT_TEST_PARITY.md b/docs/REACT_TEST_PARITY.md index cdbb7bf6..8bbc4fa5 100644 --- a/docs/REACT_TEST_PARITY.md +++ b/docs/REACT_TEST_PARITY.md @@ -29,7 +29,7 @@ re-renders/effects are observable inline. SSR behavior is tested via | ReactDOMServerIntegrationBasic | basic SSR render, nested components, nil | ✅ | ui/ssr_components_test.go | | ReactDOMServerIntegrationFragment | fragment SSR | ✅ | ui/ssr_components_test.go (component->fragment hoist) | | ReactDOMServerIntegrationSpecialTypes | numbers/bools/null children | 🚫 | Go static typing (ui.Node children) | -| ReactDOMServerIntegrationHooks | hooks under SSR | 🔶 | ui/ssr_hooks_boundary_test.go — ARCHITECTURAL GAP: ui.RenderToString is hook-less; hook/context components render only via the reconciler (RenderInto), which IS tested. String-SSR of hook components errors. Candidate for a dedicated "SSR hooks" feature (transient fiber tree + context propagation + hook server-mode). | +| ReactDOMServerIntegrationHooks | hooks under SSR | ✅ | ui/ssr_hooks_test.go — IMPLEMENTED in v3.5.0: RenderToString runs useState/useRef/useMemo/useContext (effects skipped); context flows + nested providers override. Streaming path: hooks yes, context threading is a follow-up. | | ReactDOMServerIntegrationRefs | refs inert during SSR | ✅ | ui/ssr_ref_and_context_test.go | | ReactDOMServerIntegrationNewContext | context under SSR / multi-consumer | ✅ | ui/ssr_ref_and_context_test.go, ui/context_native_test.go | | ReactDOMServerIntegrationTextarea/Input/Checkbox | controlled inputs SSR | ✅ | ui/ssr_controlled_inputs_test.go (textarea value->content: found+fixed v3.4.9) | diff --git a/examples/public-examples-site/assets/docs/latest-release-notes.md b/examples/public-examples-site/assets/docs/latest-release-notes.md index b12ec10a..e73b62ab 100644 --- a/examples/public-examples-site/assets/docs/latest-release-notes.md +++ b/examples/public-examples-site/assets/docs/latest-release-notes.md @@ -3,18 +3,19 @@ This docs-site page mirrors the first section returned by `tools/changelogcheck.LatestEntry(CHANGELOG.md)`. -## v3.4.10 - 2026-06-24 +## v3.5.0 - 2026-06-24 -The current latest changelog section is `v3.4.10 - 2026-06-24`. **Fixed:** a -controlled ``, so a -server-rendered select showed no initial selection and mismatched on hydration. -The serializer now drops `value` from the `