@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 +1 -1
- package/dist/bin/{runtime-discovery-CZ2QHT4Q.js → runtime-discovery-3HH6BP3K.js} +1 -1
- package/dist/bin/{temp-rsc-server-QRR6HD6T.js → temp-rsc-server-JALTIWLT.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +7 -0
- package/skills/app-structure/large-app.md +3 -0
- package/skills/client-urls/SKILL.md +11 -0
- package/skills/hooks/outlets.md +4 -0
- package/skills/layout/SKILL.md +49 -0
- package/skills/ux-architecture/SKILL.md +3 -1
- package/src/browser/server-action-bridge.ts +13 -8
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-
|
|
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-
|
|
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 {
|
package/dist/vite/index.js
CHANGED
|
@@ -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.
|
|
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
|
@@ -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>`,
|
package/skills/hooks/outlets.md
CHANGED
|
@@ -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:
|
package/skills/layout/SKILL.md
CHANGED
|
@@ -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
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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;
|