@rangojs/router 0.33.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/rango.js CHANGED
@@ -229,7 +229,7 @@ ${formatNestedRouterConflictError(nestedRouterConflict, "[rango]")}
229
229
  }
230
230
  let discoverAndWriteRouteTypes;
231
231
  try {
232
- const mod = await import("./runtime-discovery-CZ2QHT4Q.js");
232
+ const mod = await import("./runtime-discovery-3HH6BP3K.js");
233
233
  discoverAndWriteRouteTypes = mod.discoverAndWriteRouteTypes;
234
234
  } catch (err) {
235
235
  if (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND") {
@@ -163,7 +163,7 @@ async function discoverAndWriteRouteTypes(opts) {
163
163
  "Runtime discovery requires '@vitejs/plugin-rsc'. Install it with: pnpm add -D @vitejs/plugin-rsc"
164
164
  );
165
165
  }
166
- const { createTempRscServer } = await import("./temp-rsc-server-QRR6HD6T.js");
166
+ const { createTempRscServer } = await import("./temp-rsc-server-JALTIWLT.js");
167
167
  const { createDiscoveryState } = await import("./state-ACD2MGKJ.js");
168
168
  const { discoverClientUrlProjections } = await import("./client-urls-projection-DQLWPDAT.js");
169
169
  const {
@@ -47,7 +47,7 @@ var DEFAULT_ROUTER_VERSIONS_KEY = "*";
47
47
  // package.json
48
48
  var package_default = {
49
49
  name: "@rangojs/router",
50
- version: "0.33.0",
50
+ version: "0.33.1",
51
51
  description: "Django-inspired RSC router with composable URL patterns",
52
52
  keywords: [
53
53
  "react",
@@ -7327,7 +7327,7 @@ function resolveVersionsFrom(table, routerId2) {
7327
7327
  // package.json
7328
7328
  var package_default = {
7329
7329
  name: "@rangojs/router",
7330
- version: "0.33.0",
7330
+ version: "0.33.1",
7331
7331
  description: "Django-inspired RSC router with composable URL patterns",
7332
7332
  keywords: [
7333
7333
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -120,6 +120,13 @@ here for the rules they share.
120
120
  by name, keep page-specific blocks in their route, and write such a rule
121
121
  for chrome that shows a param's data
122
122
  ([Prefetches are shared](#prefetches-are-shared)).
123
+ 10. **A layout keeps its `<Outlet />` in one place.** Branch inside one shape
124
+ (`<>{promo ? <Banner /> : null}<Outlet /></>`), never between a bare
125
+ `return <Outlet />` and a wrapped one: the outlet moves, and every page
126
+ below remounts when the branch flips, its state reset and its mount
127
+ effects run again. When an early return is unavoidable and the other
128
+ branch is only a fragment, give the outlet the same `key` in both
129
+ (`/layout`, "Moving the Outlet remounts its children").
123
130
 
124
131
  ## A small app: one module
125
132
 
@@ -893,6 +893,9 @@ stay inside the project module can come from `useReverse(routes)` in
893
893
  user's name): the capture is refused and the page never serves a shell.
894
894
  - **`prefetch: false` on every loader**: each click then waits for a fill.
895
895
  Flag the expensive, rarely-clicked ones.
896
+ - **A layout that moves its `<Outlet />` between branches** (a bare
897
+ `return <Outlet />` on one, a wrapped one on another): every page below it
898
+ remounts when the branch flips. Keep one shape (`/layout`).
896
899
  - **A bare `isAction(refs)` predicate**: it is a hard `false` on navigation
897
900
  too, so the loader stops following param changes. Use
898
901
  `isAction() ? isAction(refs) : undefined` (`/loader`).
@@ -745,6 +745,17 @@ a hard load of the target URL renders the full route.
745
745
  throwers in the client `ErrorBoundary`; `useFetchLoader` works unchanged
746
746
  (route-independent lane; `revalidate()` deliberately not consulted).
747
747
  Pinned in the router repo's client-urls hook-probe e2e.
748
+ - **An action called on the optimistic frame runs against the committed
749
+ page.** The destination mounts at the click, before the navigation
750
+ commits, so an action its mount effect calls is sent from the page the URL
751
+ bar still shows: the server runs it with that page's request context
752
+ (`ctx.url`, `ctx.params`, its middleware) and re-renders that page, not
753
+ the destination. Nothing of that render reaches the destination, and a
754
+ write still refreshes the destination once it commits (unless the action
755
+ called `keepClientCache()`). Pass what the action needs as arguments
756
+ instead of reading the request's URL or params. A link prefetched before
757
+ the click (the production default, `viewport`) usually commits before the
758
+ effect runs.
748
759
  - **Location state follows the same split as the route hooks.** Inside the
749
760
  optimistic branch (the destination and the group's client layouts)
750
761
  `useLocationState` reads the state the navigation carries (`<Link state>`,
@@ -28,6 +28,10 @@ function DashboardLayout() {
28
28
  `ParallelOutlet` requires the name and reads clearer at the call site. See
29
29
  `/layout` and `/parallel` for the server side.
30
30
 
31
+ An outlet that moves between a layout's branches (bare in one, wrapped in
32
+ another) remounts its children; see `/layout`, "Moving the Outlet remounts its
33
+ children".
34
+
31
35
  ### useOutlet()
32
36
 
33
37
  Access outlet content and its client-route presentation state programmatically:
@@ -12,6 +12,12 @@ layout stays mounted while you navigate between the routes it wraps. This skill
12
12
  covers layout forms, outlets, orphan layouts (a layout inside a `path()`), and
13
13
  layout revalidation.
14
14
 
15
+ **Rule: a layout keeps its `<Outlet />` in one place on every branch.** Branch
16
+ around the outlet, never around it and the outlet together: an early
17
+ `return <Outlet />` beside a wrapped one remounts every child segment each time
18
+ the branch flips (state resets, mount effects and the actions they call run
19
+ again). See [Moving the Outlet remounts its children](#moving-the-outlet-remounts-its-children).
20
+
15
21
  Related skills: `/parallel` (named slots rendered by a layout), `/intercept`
16
22
  (modal slots), `/route` (the routes a layout wraps), `/router-setup` (the
17
23
  `document` shell that renders `<html>`).
@@ -131,6 +137,49 @@ function ShopLayout() {
131
137
 
132
138
  A layout's `transition()` config wraps the content that flows through `<Outlet />` — not the layout chrome itself, and not sibling `<ParallelOutlet />` slots. Stacking transitions across nested layouts collapses around the deepest default outlet content. See [skills/view-transitions](../view-transitions/SKILL.md) for the full wrap rules and intercept-modal interaction.
133
139
 
140
+ ### Moving the Outlet remounts its children
141
+
142
+ React keeps a subtree mounted only while each element on its path keeps the
143
+ same type at the same position. A layout that returns a bare `<Outlet />` on
144
+ one branch and wraps it on another (a fragment, a `<div>`) moves the outlet,
145
+ so when the branch flips, every child segment unmounts and mounts again:
146
+ client state resets, mount effects run again (an action they call is sent
147
+ again), and the content flashes. Prefer one shape with the branch inside it:
148
+
149
+ ```tsx
150
+ // Remounts the children whenever `promo` flips
151
+ function CategoryLayout({ promo }: { promo: boolean }) {
152
+ if (!promo) return <Outlet />;
153
+ return (
154
+ <>
155
+ <PromoBanner />
156
+ <Outlet />
157
+ </>
158
+ );
159
+ }
160
+
161
+ // Keeps them mounted: the outlet keeps its position
162
+ function CategoryLayout({ promo }: { promo: boolean }) {
163
+ return (
164
+ <>
165
+ {promo ? <PromoBanner /> : null}
166
+ <Outlet />
167
+ </>
168
+ );
169
+ }
170
+ ```
171
+
172
+ A stable `key` on the outlet (`<Outlet key="outlet" />` in both branches)
173
+ also keeps the children mounted when the only difference is a top-level
174
+ fragment: React reads a component's unkeyed top-level fragment as a list of
175
+ siblings and matches the keyed outlet across it. It does not help when one
176
+ branch wraps the outlet in an element (a `<div>`, a provider): the outlet then
177
+ sits one level deeper, and keys match only among siblings.
178
+
179
+ All of this applies to `<ParallelOutlet />` too. To reset the children on
180
+ purpose, change a `key` on a stable wrapper rather than relying on a shape
181
+ change.
182
+
134
183
  ## Named Outlets
135
184
 
136
185
  For parallel routes, use named outlets (`<Outlet name="@sidebar" />` is
@@ -941,7 +941,9 @@ Each entry: what the user sees, why, the fix.
941
941
  8. **Shared chrome in a route component.** It remounts on every navigation to
942
942
  another route: its state resets and its own fallbacks show again. Fix: move
943
943
  it into a `layout()` (a group `layout()` keeps its instance across its
944
- routes; a server layout across everything below it).
944
+ routes; a server layout across everything below it). A layout that moves
945
+ its `<Outlet />` between branches remounts the children the same way; see
946
+ `/layout`, "Moving the Outlet remounts its children".
945
947
  9. **`useEffect` fetching.** A spinner after the page has shown, nothing
946
948
  prefetched or server-rendered, no re-run after an action, races on fast
947
949
  navigations. Fix: a loader read with `useLoader`; `useFetchLoader` for
@@ -885,14 +885,19 @@ export function createServerActionBridge(
885
885
  throw new Error("No matched segments in response");
886
886
  }
887
887
 
888
- // Reconcile server segments with cached segments (single source of truth)
889
- const reconciled = reconcileSegments({
890
- actor: "action",
891
- matched,
892
- diff: diff || [],
893
- serverSegments: segments || [],
894
- cachedSegments,
895
- });
888
+ // Reconcile server segments with cached segments (single source of
889
+ // truth). A page that left commits nothing (navigated-away), and the
890
+ // cache on screen is another page's: reconciling would report missing
891
+ // segments.
892
+ const reconciled = navigatedSince()
893
+ ? { segments: [], mainSegments: [], interceptSegments: [] }
894
+ : reconcileSegments({
895
+ actor: "action",
896
+ matched,
897
+ diff: diff || [],
898
+ serverSegments: segments || [],
899
+ cachedSegments,
900
+ });
896
901
  const fullSegments = reconciled.segments;
897
902
 
898
903
  const returnData = returnValue?.data;