fix(hydrogen): route-scope WeaverseHydrogenRoot weaverseData lookup (#451)#452
Conversation
…451) `WeaverseHydrogenRoot` previously resolved `weaverseData` by walking `useMatches()` and returning the deepest match's payload. Because `useMatches()` is tree-global, every instance on the page resolved to the same value — making it impossible for two `<WeaverseContent />` instances (e.g. a layout-loaded page + a child-loaded page) to render different Weaverse pages on the same URL. Restore the route-scoped primitive: call `useLoaderData()` to read the *own* route's loader data, which is what React Router's framework-mode docs prescribe for components that need their rendering route's data. The `useMatches()` walk is retained as a third-tier fallback so legacy patterns (e.g. an index leaf without its own loader falling back to its layout's `weaverseData`) keep working unchanged. Resolution order, implemented in a pure helper `pickWeaverseData()`: 1. Explicit `data` prop (new — escape hatch + supports the typed `Route.ComponentProps.loaderData` framework-mode pattern) 2. Own route's loader data via `useLoaderData()` (route-scoped primitive) 3. Ancestor walk through `useMatches()` (back-compat for the index-without-loader case) `createWeaverseDataContext(matches)` is left untouched — it legitimately needs all matches to build the route-keyed data-connector context. Back-compat: - Unmodified themes with zero-prop `<WeaverseContent />` continue to work via Tier 2; identical render output to the previous version for the common single-instance case. - The only intentional behaviour change is the #451 case: two `<WeaverseHydrogenRoot>` instances on the same URL now each render their own route's page instead of both rendering the deepest one. - `data?` prop is optional; existing call sites compile unchanged (verified via generated `.d.ts`). Tests: 18 new selector unit tests covering all three tiers, including an explicit #451 regression and dedicated back-compat scenarios. Fixes #451
|
Claude finished @paul-phan's task in 2m 34s —— View job Code Review: fix(hydrogen): route-scope WeaverseHydrogenRoot weaverseData lookup (#451)
SummaryThis is a well-motivated, carefully scoped fix. The root cause diagnosis is accurate (the
|
Three low-severity items from the claude-review on #452: 1. `hasWeaverseData` used a bare `in` check that returned true for `{ weaverseData: undefined }`, letting Tier 2 short-circuit with `undefined` and bypass the Tier 3 ancestor walk. Tighten to require the value be defined. 2. The `data` prop JSDoc promised `null` would "suppress rendering", but the component fell through to `<ErrorComponent>`. Add an explicit early-return for `dataProp === null` so the documented behaviour matches. Auto-resolved `null` (data was expected but not found) still renders the error component — those are different semantics and stay distinct. 3. Add three regression tests covering `weaverseData: undefined` at both Tier 2 and Tier 3, plus the null-suppression flow. Tests: 94/94 hydrogen tests pass (was 91).
hta218
left a comment
There was a problem hiding this comment.
Thanks Paul for the thorough fix and excellent write-up! 🙌
The route-scoped useLoaderData() approach is the right call, and the 3-tier pickWeaverseData() selector keeps full back-compat while cleanly solving #451. The pure, hook-free helper with 18 unit tests covering every tier, edge case, and the regression scenario is exactly how this should be done.
One non-blocking follow-up to consider: the pure selector is well covered, but the actual useLoaderData() route-scoping (the crux of the fix) isn't exercised by an automated test — a future integration test with createMemoryRouter + nested routes would lock it down. Fine to merge now since the RR v7 behavior is well-established.
Also remember to land the new data prop + #451 behavior change in packages/hydrogen/CHANGELOG.md at release time.
Approving — great work! 🚀
Fixes #451.
What changed
WeaverseHydrogenRootpreviously resolvedweaverseDataby walkinguseMatches()and returning the deepest match's payload. BecauseuseMatches()returns the same array for every component on the page, every<WeaverseContent />instance resolved to the same value — making it impossible for two instances (e.g. a layout-loaded page + a child-loaded page) to render different Weaverse pages on the same URL.This PR restores the route-scoped primitive via
useLoaderData()for theweaverseDatalookup, while keeping the existinguseMatches()-drivencreateWeaverseDataContextinfrastructure untouched (it legitimately needs all matches to build the route-keyed data-connector context).How it resolves now
A pure helper
pickWeaverseData()implements a three-tier policy:dataprop (new — escape hatch) — supports the typedRoute.ComponentProps.loaderDataframework-mode pattern and gives consumers full control.useLoaderData()— React Router's route-scoped primitive; this is what makes layout + child compositions work.useMatches()— back-compat fallback for the index-without-loader pattern (e.g. anindex.tsxthat omitsweaverseDataand inherits from its layout).Back-compat guarantee (verified)
<WeaverseContent />) work identically via Tier 2 for the common single-instance pattern.weaverseDatavia Tier 3.Promise) loaders pass through unchanged at every tier.data?prop is optional; existing call sites compile unchanged (verified via generated.d.ts).<WeaverseHydrogenRoot>instances on the same URL now each render their own route's page instead of both rendering the deepest one — exactly theWeaverseHydrogenRootresolves only the deepest match'sweaverseData, breaking nested layout + child Weaverse pages #451 fix.The pilot wrapper (
templates/pilot/app/weaverse/index.tsx) is intentionally not modified in this PR. The fix works end-to-end for unmodified pilots; updating the wrapper to forward an explicitdataprop is a separate optional follow-up.Tests
18 new selector unit tests in
packages/hydrogen/__tests__/pick-weaverse-data.test.ts:null+Promise, own loader data, ancestor walk)WeaverseHydrogenRootresolves only the deepest match'sweaverseData, breaking nested layout + child Weaverse pages #451 regression assertion: two instances with differentownLoaderDatadiverge despite an identicalmatchesarrayLocal verification:
pnpm run biome && pnpm run typecheck && pnpm run test— 91/91 hydrogen tests pass.Background — root cause history
The buggy resolver was introduced in commit
a21c8d5d(Sep 23 2025, "feat: enhance data connector with deep recursive replacement and performance optimizations"), which migrated fromuseLoaderData()to auseMatches()walk under the banner "60-70% memory improvement". Reading the diff, the real architectural motivation was building a flat route-keyed data context for{{root.layout.shop.name}}-style templates — which legitimately needs all matches. TheweaverseDatalookup was collapsed into the same walk only becausematcheswas already in scope; that's the regression. The memory improvement claim appears only in CHANGELOGs (no benchmark in the repo) and pertains to the data-context rebuild, not to theweaverseDatalookup specifically.Commit
4dfcbea7(Nov 12 2025) later reversed the iteration direction, flipping the failure mode from "layout page renders twice" to "child page renders twice" — which is what #451 reports.Second commit
chore: replace bun with pnpm in releasing-weaverse-sdks skill— follow-up to PR #450's package manager migration; this skill file was missed.