Skip to content

Nested route transitions render children before the parent's load completes #920

Description

@lemonmade

Summary

When navigating to a nested route whose parent declares a load function, I'm seeing components that are mounted inside the parent's render (or persistent frame components that read the URL directly) render against a cold cache — the URL commit / transition appears to happen before the parent load's awaited work has settled.

Context / repro

In our app, the route tree looks roughly like:

routeWithAppContext(true, {
  render: (children) => <BusinessFrame>{children}</BusinessFrame>,
  async load(_entry, context) {
    await context.graphql.cache.query(frameQuery);
  },
  children: [
    routeWithAppContext('/activities', {
      children: [
        routeWithAppContext(':id', {
          async load({context, matched}) {
            await Promise.all([
              BusinessActivityDetailPage.load(),
              context.graphql.cache.query(businessActivityFrameQuery, {
                variables: {id: GlobalID.serialize('Activity', matched)},
              }),
            ]);
          },
          render: (children) => <BusinessActivityDetailPage>{children}</BusinessActivityDetailPage>,
          children: [
            routeWithAppContext('/', { /* overview — its own load */ }),
            routeWithAppContext('/schedule', { children: [/* ... */] }),
            routeWithAppContext('/settings', { /* ... */ }),
          ],
        }),
      ],
    }),
  ],
});

BusinessFrame renders a persistent side nav that includes a contextual mini-nav component. That component reads the URL via useRelativePathname(), extracts the activity id, and constructs a model that calls context.graphql.cache.create(activityFrameQuery).run({id}) — the same query the parent load preloads.

On a cold navigation to /activities/:id/<something>, the mini-nav and the BusinessActivityDetailPage frame render with activity as null (the query observable hasn't resolved) for a beat, then pop in. The data is being preloaded — calling cache.query(...) in the parent's load and awaiting it — but the transition seems to commit before that preload is actually in the cache that cache.create(...).run(...) reads from.

We worked around it by duplicating the cache.query(businessActivityFrameQuery, ...) call in each of the leaf child routes' loads. That makes the pop-in go away, which matches the theory: the child load is blocking the transition correctly, the parent load isn't.

Expected

Route transitions shouldn't commit / render until every matched ancestor route's load has resolved. If parent loads are intentionally fire-and-forget relative to child transitions, that'd be worth documenting — but the surprise is large enough that I suspect it's a bug.

Questions for triage

  • Are parent and child loads supposed to run in parallel and both gate the transition, or does the framework only gate on the leaf?
  • If parallel-gating is the intent, is there a case where the parent's awaited side effects (like hydrating a query cache) aren't visible to the already-committed render?

Happy to provide a minimal repro if useful — just wanted to surface the shape of the issue first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions