Skip to content

fs-routes: client components receive stale params on soft navigation, with no way to read live ones #174

Description

@k35o

Problem

Repro: https://stackblitz.com/edit/ykdufm36

A minimal fsRoutes project (@funstack/static 1.3.0) with a single dynamic segment. src/pages/[lang]/layout.tsx is a client component printing useLocation().pathname and params.lang; src/pages/[lang]/page.tsx is a server component printing params.lang, with generateStaticParams returning en and ja.

Open /ja, then click the en link:

before click:  URL: /ja  / params.lang: ja   |  page params.lang: ja
after click:   URL: /en  / params.lang: ja   |  page params.lang: ja

The URL changes but params does not, in the layout and in the page. Loading /en directly is correct, which makes it easy to miss.

This matches the note in File-System Routing, so the symptom itself is known. It lands hard when the dynamic segment sits at the root of the tree, though. In an i18n site every route lives under a segment like [lang] above, so switching language keeps rendering the previous locale, and nothing fails anywhere to signal it. And there is no supported way to read the live params instead, since useRouteParams() takes a route definition object and with fsRoutes the definitions are built inside the library.

Cause

Opening /ja gives the browser /ja's HTML and its RSC payload. The payload is fetched exactly once, by prodMain in packages/static/src/client/entry.tsx; nothing ever fetches one again. Clicking en swaps the URL on the client and loads no new document, so no new HTML or payload arrives either: after the navigation the browser still holds only /ja's payload. /en's payload does exist in the build output (funstack__/fun__rsc-payload/<hash>.txt), but nothing can reach it, since each HTML carries only its own payload URL and there is no index from URL to payload.

Both stale values on screen come from that one payload, but not for the same reason.

First, the layout. buildRouteDefinitions in packages/static/src/fs-routes/runtime.tsx writes the params into the route definition as element props while the payload is being built:

definition.component = createElement(Component, { params });

The layout is a client component, so it is shipped to the browser and does re-render on navigation, which is why its pathname updates. The props it re-renders with are the baked ones, and nothing replaces them. Fresh params for the layout would not require /en's payload at all: the router can supply them from the current match, and already does so when component is a component type rather than an element.

Then the page. As a server component it ran at build time and the payload holds its rendered output rather than the component. There is nothing in the browser to re-render with different params, and server components cannot run there. Producing /en's text requires /en's payload.

In short, the two values are equally stale but not for equal reasons. The page's ja is forced by static hosting: no implementation fixes it without loading /en's payload. The layout's ja is not forced; it follows from runtime.tsx passing an element, and could change with static hosting left exactly as it is. Two questions, then:

  1. Would taking client components out of this limitation be in scope for fsRoutes? That half does not seem bound by the one-payload constraint.
  2. Independently of that, could there be a supported way to read the live params under fsRoutes?

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