@funstack/router 0.0.7-alpha.0 → 0.0.8
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/docs/ApiComponentsPage.tsx +32 -0
- package/dist/docs/ApiHooksPage.tsx +8 -3
- package/dist/docs/ApiTypesPage.tsx +56 -8
- package/dist/docs/ApiUtilitiesPage.tsx +26 -8
- package/dist/docs/GettingStartedPage.tsx +16 -1
- package/dist/docs/LearnNestedRoutesPage.tsx +2 -4
- package/dist/docs/LearnRscPage.tsx +16 -3
- package/dist/docs/LearnSsrPage.tsx +99 -20
- package/dist/docs/LearnTransitionsPage.tsx +80 -6
- package/dist/docs/LearnTypeSafetyPage.tsx +12 -2
- package/dist/docs/index.md +2 -2
- package/dist/index.d.mts +28 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +86 -22
- package/dist/index.mjs.map +1 -1
- package/dist/{route-ClVnhrQD.d.mts → route-DRcgs0Pt.d.mts} +61 -6
- package/dist/route-DRcgs0Pt.d.mts.map +1 -0
- package/dist/route-p_gr5yPI.mjs.map +1 -1
- package/dist/server.d.mts +1 -1
- package/package.json +4 -4
- package/dist/route-ClVnhrQD.d.mts.map +0 -1
|
@@ -55,6 +55,38 @@ export function ApiComponentsPage() {
|
|
|
55
55
|
the router will intercept the navigation.
|
|
56
56
|
</td>
|
|
57
57
|
</tr>
|
|
58
|
+
<tr>
|
|
59
|
+
<td>
|
|
60
|
+
<code>fallback</code>
|
|
61
|
+
</td>
|
|
62
|
+
<td>
|
|
63
|
+
<code>{'"none" | "static"'}</code>
|
|
64
|
+
</td>
|
|
65
|
+
<td>
|
|
66
|
+
Fallback mode when Navigation API is unavailable.{" "}
|
|
67
|
+
<code>"none"</code> (default) renders nothing;{" "}
|
|
68
|
+
<code>"static"</code> renders matched routes using{" "}
|
|
69
|
+
<code>window.location</code> without navigation interception
|
|
70
|
+
(MPA behavior).
|
|
71
|
+
</td>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr>
|
|
74
|
+
<td>
|
|
75
|
+
<code>ssrPathname</code>
|
|
76
|
+
</td>
|
|
77
|
+
<td>
|
|
78
|
+
<code>string</code>
|
|
79
|
+
</td>
|
|
80
|
+
<td>
|
|
81
|
+
Pathname to use for route matching during SSR. When provided,
|
|
82
|
+
path-based routes match against this pathname on the server.
|
|
83
|
+
Routes with loaders are always skipped during SSR. Once the
|
|
84
|
+
client hydrates, the real URL from the Navigation API takes
|
|
85
|
+
over. See the{" "}
|
|
86
|
+
<a href="/learn/server-side-rendering">SSR guide</a> for
|
|
87
|
+
details.
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|
|
58
90
|
</tbody>
|
|
59
91
|
</table>
|
|
60
92
|
</article>
|
|
@@ -163,9 +163,14 @@ function MyComponent() {
|
|
|
163
163
|
<ul>
|
|
164
164
|
<li>
|
|
165
165
|
This hook is powered by React's <code>useTransition</code>. The
|
|
166
|
-
router wraps
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
router wraps navigations in <code>startTransition</code>, so React
|
|
167
|
+
defers rendering suspended routes and keeps the current UI visible.
|
|
168
|
+
</li>
|
|
169
|
+
<li>
|
|
170
|
+
Sync state updates via <code>setStateSync</code> and{" "}
|
|
171
|
+
<code>resetStateSync</code> bypass transitions entirely, so{" "}
|
|
172
|
+
<code>isPending</code> will <strong>not</strong> become{" "}
|
|
173
|
+
<code>true</code> for those updates.
|
|
169
174
|
</li>
|
|
170
175
|
<li>
|
|
171
176
|
The same <code>isPending</code> value is also available as a prop on
|
|
@@ -37,7 +37,10 @@ type Props = {
|
|
|
37
37
|
state: { scrollPosition: number } |
|
|
38
38
|
((prev: { scrollPosition: number } | undefined) => { scrollPosition: number })
|
|
39
39
|
) => void;
|
|
40
|
-
|
|
40
|
+
// Async reset via replace navigation
|
|
41
|
+
resetState: () => Promise<void>;
|
|
42
|
+
// Sync reset via updateCurrentEntry
|
|
43
|
+
resetStateSync: () => void;
|
|
41
44
|
info: unknown; // Ephemeral navigation info
|
|
42
45
|
isPending: boolean; // Whether a navigation transition is pending
|
|
43
46
|
};`}</CodeBlock>
|
|
@@ -48,11 +51,27 @@ type Props = {
|
|
|
48
51
|
<li>
|
|
49
52
|
<code>setState</code> - Async method that returns a Promise. Uses
|
|
50
53
|
replace navigation internally, ensuring the state update goes
|
|
51
|
-
through the full navigation cycle.
|
|
54
|
+
through the full navigation cycle. Because it performs a navigation,
|
|
55
|
+
it is wrapped in a React transition and may set{" "}
|
|
56
|
+
<code>isPending</code> to <code>true</code>.
|
|
52
57
|
</li>
|
|
53
58
|
<li>
|
|
54
59
|
<code>setStateSync</code> - Synchronous method that updates state
|
|
55
|
-
immediately using <code>navigation.updateCurrentEntry()</code>.
|
|
60
|
+
immediately using <code>navigation.updateCurrentEntry()</code>. This
|
|
61
|
+
is <strong>not</strong> a navigation, so it bypasses React
|
|
62
|
+
transitions and will never set <code>isPending</code> to{" "}
|
|
63
|
+
<code>true</code>.
|
|
64
|
+
</li>
|
|
65
|
+
<li>
|
|
66
|
+
<code>resetState</code> - Async method that clears navigation state
|
|
67
|
+
via replace navigation. Like <code>setState</code>, it goes through
|
|
68
|
+
a React transition and may set <code>isPending</code> to{" "}
|
|
69
|
+
<code>true</code>.
|
|
70
|
+
</li>
|
|
71
|
+
<li>
|
|
72
|
+
<code>resetStateSync</code> - Clears navigation state synchronously.
|
|
73
|
+
Like <code>setStateSync</code>, this bypasses React transitions and
|
|
74
|
+
will never set <code>isPending</code> to <code>true</code>.
|
|
56
75
|
</li>
|
|
57
76
|
</ul>
|
|
58
77
|
</article>
|
|
@@ -80,7 +99,8 @@ type Props = {
|
|
|
80
99
|
state: { selectedTab: string } | undefined;
|
|
81
100
|
setState: (state: ...) => Promise<void>; // async
|
|
82
101
|
setStateSync: (state: ...) => void; // sync
|
|
83
|
-
resetState: () => void
|
|
102
|
+
resetState: () => Promise<void>; // async
|
|
103
|
+
resetStateSync: () => void; // sync
|
|
84
104
|
info: unknown; // Ephemeral navigation info
|
|
85
105
|
isPending: boolean; // Whether a navigation transition is pending
|
|
86
106
|
};`}</CodeBlock>
|
|
@@ -238,8 +258,9 @@ type SettingsPageProps = RouteComponentPropsOf<typeof settingsRoute>;
|
|
|
238
258
|
<code>component: UserPage</code>): Router automatically injects
|
|
239
259
|
props (<code>params</code>, <code>state</code>,{" "}
|
|
240
260
|
<code>setState</code>, <code>setStateSync</code>,{" "}
|
|
241
|
-
<code>resetState</code>, <code>
|
|
242
|
-
and <code>data</code>
|
|
261
|
+
<code>resetState</code>, <code>resetStateSync</code>,{" "}
|
|
262
|
+
<code>info</code>, <code>isPending</code>, and <code>data</code>{" "}
|
|
263
|
+
when a loader is defined).
|
|
243
264
|
</li>
|
|
244
265
|
<li>
|
|
245
266
|
<strong>JSX element</strong> (e.g.,{" "}
|
|
@@ -268,15 +289,42 @@ routeState<{ tab: string }>()({
|
|
|
268
289
|
});`}</CodeBlock>
|
|
269
290
|
</article>
|
|
270
291
|
|
|
292
|
+
<article className="api-item">
|
|
293
|
+
<h3>
|
|
294
|
+
<code>ActionArgs</code>
|
|
295
|
+
</h3>
|
|
296
|
+
<p>
|
|
297
|
+
Arguments passed to route action functions. The <code>request</code>{" "}
|
|
298
|
+
carries the POST method and <code>FormData</code> body from the form
|
|
299
|
+
submission.
|
|
300
|
+
</p>
|
|
301
|
+
<CodeBlock language="typescript">{`interface ActionArgs<Params> {
|
|
302
|
+
params: Params;
|
|
303
|
+
request: Request; // method: "POST", body: FormData
|
|
304
|
+
signal: AbortSignal;
|
|
305
|
+
}`}</CodeBlock>
|
|
306
|
+
</article>
|
|
307
|
+
|
|
271
308
|
<article className="api-item">
|
|
272
309
|
<h3>
|
|
273
310
|
<code>LoaderArgs</code>
|
|
274
311
|
</h3>
|
|
275
|
-
<
|
|
276
|
-
|
|
312
|
+
<p>
|
|
313
|
+
Arguments passed to route loader functions. The optional{" "}
|
|
314
|
+
<code>actionResult</code> parameter contains the return value of the
|
|
315
|
+
route's action when the loader runs after a form submission.
|
|
316
|
+
</p>
|
|
317
|
+
<CodeBlock language="typescript">{`interface LoaderArgs<Params, ActionResult = undefined> {
|
|
318
|
+
params: Params;
|
|
277
319
|
request: Request;
|
|
278
320
|
signal: AbortSignal;
|
|
321
|
+
actionResult: ActionResult | undefined;
|
|
279
322
|
}`}</CodeBlock>
|
|
323
|
+
<p>
|
|
324
|
+
On normal navigations, <code>actionResult</code> is{" "}
|
|
325
|
+
<code>undefined</code>. After a form submission, it contains the
|
|
326
|
+
action's return value (awaited if the action is async).
|
|
327
|
+
</p>
|
|
280
328
|
</article>
|
|
281
329
|
|
|
282
330
|
<article className="api-item">
|
|
@@ -17,8 +17,9 @@ export function ApiUtilitiesPage() {
|
|
|
17
17
|
always receives a <code>params</code> prop with types inferred from
|
|
18
18
|
the path pattern. When a <code>loader</code> is defined, the component
|
|
19
19
|
also receives a <code>data</code> prop. Components also receive{" "}
|
|
20
|
-
<code>state</code>, <code>setState</code>, <code>setStateSync</code>,
|
|
21
|
-
and <code>
|
|
20
|
+
<code>state</code>, <code>setState</code>, <code>setStateSync</code>,{" "}
|
|
21
|
+
<code>resetState</code>, and <code>resetStateSync</code> props for
|
|
22
|
+
navigation state management.
|
|
22
23
|
</p>
|
|
23
24
|
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
24
25
|
|
|
@@ -84,6 +85,20 @@ const myRoute = route({
|
|
|
84
85
|
(and <code>data</code> prop if loader is defined)
|
|
85
86
|
</td>
|
|
86
87
|
</tr>
|
|
88
|
+
<tr>
|
|
89
|
+
<td>
|
|
90
|
+
<code>action</code>
|
|
91
|
+
</td>
|
|
92
|
+
<td>
|
|
93
|
+
<code>(args: ActionArgs) => T</code>
|
|
94
|
+
</td>
|
|
95
|
+
<td>
|
|
96
|
+
Function to handle form submissions (POST navigations). Receives
|
|
97
|
+
a <code>Request</code> with <code>FormData</code> body. The
|
|
98
|
+
return value is passed to the loader as{" "}
|
|
99
|
+
<code>actionResult</code>.
|
|
100
|
+
</td>
|
|
101
|
+
</tr>
|
|
87
102
|
<tr>
|
|
88
103
|
<td>
|
|
89
104
|
<code>loader</code>
|
|
@@ -204,11 +219,15 @@ const productRoute = routeState<{ filter: string }>()({
|
|
|
204
219
|
<li>
|
|
205
220
|
<code>setState</code> - Async method that uses replace navigation.
|
|
206
221
|
Returns a Promise that resolves when the navigation completes.
|
|
222
|
+
Because it performs a navigation, the update is wrapped in a React
|
|
223
|
+
transition (may set <code>isPending</code> to <code>true</code>).
|
|
207
224
|
</li>
|
|
208
225
|
<li>
|
|
209
226
|
<code>setStateSync</code> - Sync method that uses{" "}
|
|
210
227
|
<code>navigation.updateCurrentEntry()</code>. Updates state
|
|
211
|
-
immediately without waiting.
|
|
228
|
+
immediately without waiting. This is not a navigation, so it
|
|
229
|
+
bypasses React transitions entirely (<code>isPending</code> stays{" "}
|
|
230
|
+
<code>false</code>).
|
|
212
231
|
</li>
|
|
213
232
|
</ul>
|
|
214
233
|
<p>Navigation state characteristics:</p>
|
|
@@ -276,8 +295,9 @@ const routes = [
|
|
|
276
295
|
<code>routeState</code> - Route definition helper with typed state
|
|
277
296
|
</li>
|
|
278
297
|
<li>
|
|
279
|
-
Types: <code>
|
|
280
|
-
<code>
|
|
298
|
+
Types: <code>ActionArgs</code>, <code>LoaderArgs</code>,{" "}
|
|
299
|
+
<code>RouteDefinition</code>, <code>PathParams</code>,{" "}
|
|
300
|
+
<code>RouteComponentProps</code>,{" "}
|
|
281
301
|
<code>RouteComponentPropsWithData</code>
|
|
282
302
|
</li>
|
|
283
303
|
</ul>
|
|
@@ -287,9 +307,7 @@ const routes = [
|
|
|
287
307
|
</p>
|
|
288
308
|
<p>
|
|
289
309
|
See the{" "}
|
|
290
|
-
<a href="/
|
|
291
|
-
React Server Components
|
|
292
|
-
</a>{" "}
|
|
310
|
+
<a href="/learn/react-server-components">React Server Components</a>{" "}
|
|
293
311
|
guide for a full walkthrough of using the server entry point.
|
|
294
312
|
</p>
|
|
295
313
|
</article>
|
|
@@ -15,6 +15,21 @@ pnpm add @funstack/router
|
|
|
15
15
|
yarn add @funstack/router`}</CodeBlock>
|
|
16
16
|
</section>
|
|
17
17
|
|
|
18
|
+
<section>
|
|
19
|
+
<h2>AI Coding Agent Support</h2>
|
|
20
|
+
<p>
|
|
21
|
+
<code>@funstack/router</code> ships with an Agent skill that gives
|
|
22
|
+
your coding assistant (Claude Code, Cursor, GitHub Copilot, etc.)
|
|
23
|
+
knowledge about the router's API and best practices. After installing
|
|
24
|
+
the package, run:
|
|
25
|
+
</p>
|
|
26
|
+
<CodeBlock language="bash">{`npx funstack-router-skill-installer`}</CodeBlock>
|
|
27
|
+
<p>
|
|
28
|
+
The installer will guide you through setting up the skill for your
|
|
29
|
+
preferred AI agent.
|
|
30
|
+
</p>
|
|
31
|
+
</section>
|
|
32
|
+
|
|
18
33
|
<section>
|
|
19
34
|
<h2>Browser Support</h2>
|
|
20
35
|
<p>
|
|
@@ -27,7 +42,7 @@ yarn add @funstack/router`}</CodeBlock>
|
|
|
27
42
|
Navigation API
|
|
28
43
|
</a>{" "}
|
|
29
44
|
which is supported in Chrome 102+, Edge 102+, Firefox 147+, Safari
|
|
30
|
-
26.2
|
|
45
|
+
26.2+.
|
|
31
46
|
</p>
|
|
32
47
|
</section>
|
|
33
48
|
|
|
@@ -439,10 +439,8 @@ function TeamLayout(props: {
|
|
|
439
439
|
Pathless routes also play a key role in server-side rendering. During
|
|
440
440
|
SSR, only pathless routes render (since no URL is available on the
|
|
441
441
|
server), making them ideal for defining the app shell. See the{" "}
|
|
442
|
-
<a href="/
|
|
443
|
-
|
|
444
|
-
</a>{" "}
|
|
445
|
-
page for details.
|
|
442
|
+
<a href="/learn/server-side-rendering">Server-Side Rendering</a> page
|
|
443
|
+
for details.
|
|
446
444
|
</p>
|
|
447
445
|
</section>
|
|
448
446
|
|
|
@@ -252,6 +252,21 @@ export default function App() {
|
|
|
252
252
|
components. The route definitions are constructed on the server and
|
|
253
253
|
passed into <code>Router</code>, which acts as the client boundary.
|
|
254
254
|
</p>
|
|
255
|
+
<p>
|
|
256
|
+
If the server knows the requested pathname, you can pass it via the{" "}
|
|
257
|
+
<code>ssrPathname</code> prop so that path-based routes render during
|
|
258
|
+
SSR (see the <a href="/learn/server-side-rendering">SSR guide</a> for
|
|
259
|
+
details):
|
|
260
|
+
</p>
|
|
261
|
+
<CodeBlock language="tsx">{`export default function App({ pathname }: { pathname: string }) {
|
|
262
|
+
return (
|
|
263
|
+
<Router
|
|
264
|
+
routes={routes}
|
|
265
|
+
fallback="static"
|
|
266
|
+
ssrPathname={pathname}
|
|
267
|
+
/>
|
|
268
|
+
);
|
|
269
|
+
}`}</CodeBlock>
|
|
255
270
|
</section>
|
|
256
271
|
|
|
257
272
|
<section>
|
|
@@ -281,9 +296,7 @@ export default function App() {
|
|
|
281
296
|
</li>
|
|
282
297
|
<li>
|
|
283
298
|
See also the{" "}
|
|
284
|
-
<a href="/
|
|
285
|
-
Server-Side Rendering
|
|
286
|
-
</a>{" "}
|
|
299
|
+
<a href="/learn/server-side-rendering">Server-Side Rendering</a>{" "}
|
|
287
300
|
guide for how the router handles SSR and hydration
|
|
288
301
|
</li>
|
|
289
302
|
</ul>
|
|
@@ -9,7 +9,9 @@ export function LearnSsrPage() {
|
|
|
9
9
|
FUNSTACK Router supports server-side rendering with a two-stage model.
|
|
10
10
|
During SSR, pathless (layout) routes without loaders render to produce
|
|
11
11
|
an app shell, while path-based routes and loaders activate only after
|
|
12
|
-
client hydration.
|
|
12
|
+
client hydration. You can optionally provide an <code>ssrPathname</code>{" "}
|
|
13
|
+
prop to match path-based routes during SSR for richer server-rendered
|
|
14
|
+
output.
|
|
13
15
|
</p>
|
|
14
16
|
|
|
15
17
|
<section>
|
|
@@ -19,12 +21,15 @@ export function LearnSsrPage() {
|
|
|
19
21
|
renders on the server from what renders on the client:
|
|
20
22
|
</p>
|
|
21
23
|
<p>
|
|
22
|
-
<strong>Stage 1 — Server:</strong>
|
|
23
|
-
server. The router matches only pathless routes
|
|
24
|
-
<code>path</code> property) that do not have a
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
<strong>Stage 1 — Server:</strong> By default, no URL is
|
|
25
|
+
available on the server. The router matches only pathless routes
|
|
26
|
+
(routes without a <code>path</code> property) that do not have a
|
|
27
|
+
loader. This produces the app shell — layouts, headers,
|
|
28
|
+
navigation chrome, and other structural markup. When{" "}
|
|
29
|
+
<code>ssrPathname</code> is provided, the router also matches
|
|
30
|
+
path-based routes against that pathname, enabling richer
|
|
31
|
+
server-rendered content. In both cases, routes with loaders are always
|
|
32
|
+
skipped during SSR.
|
|
28
33
|
</p>
|
|
29
34
|
<p>
|
|
30
35
|
<strong>Stage 2 — Client hydration:</strong> Once the browser
|
|
@@ -34,13 +39,13 @@ export function LearnSsrPage() {
|
|
|
34
39
|
</p>
|
|
35
40
|
<CodeBlock language="tsx">{`// What renders at each stage:
|
|
36
41
|
|
|
37
|
-
// Stage 1 (Server)
|
|
38
|
-
//
|
|
39
|
-
// App shell (pathless
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// ✗ No loaders
|
|
43
|
-
// ✗ No URL available
|
|
42
|
+
// Stage 1 (Server) Stage 2 (Client)
|
|
43
|
+
// ─────────────────────────── ─────────────────
|
|
44
|
+
// App shell (pathless routes) App shell (pathless)
|
|
45
|
+
// + path routes if ssrPathname ✓ Path routes match
|
|
46
|
+
// is provided (no loaders)
|
|
47
|
+
// ✗ No loaders ✓ Loaders execute
|
|
48
|
+
// ✗ No URL available ✓ URL from Navigation API`}</CodeBlock>
|
|
44
49
|
</section>
|
|
45
50
|
|
|
46
51
|
<section>
|
|
@@ -73,6 +78,76 @@ export function LearnSsrPage() {
|
|
|
73
78
|
</p>
|
|
74
79
|
</section>
|
|
75
80
|
|
|
81
|
+
<section>
|
|
82
|
+
<h3>
|
|
83
|
+
Path-Based SSR with <code>ssrPathname</code>
|
|
84
|
+
</h3>
|
|
85
|
+
<p>
|
|
86
|
+
By default, only pathless routes render during SSR because the server
|
|
87
|
+
has no URL. The <code>ssrPathname</code> prop lets you provide a
|
|
88
|
+
pathname so path-based routes can also match during SSR, producing
|
|
89
|
+
fuller server-rendered HTML.
|
|
90
|
+
</p>
|
|
91
|
+
<CodeBlock language="tsx">{`// Server knows the requested URL and passes it to the router
|
|
92
|
+
<Router routes={routes} ssrPathname="/about" />`}</CodeBlock>
|
|
93
|
+
<p>
|
|
94
|
+
When <code>ssrPathname</code> is provided, the router matches
|
|
95
|
+
path-based routes against it just as it would match against the real
|
|
96
|
+
URL on the client. Route params are extracted normally. However,
|
|
97
|
+
routes with loaders are always skipped during SSR regardless of this
|
|
98
|
+
setting — there is no request context available to run them.
|
|
99
|
+
</p>
|
|
100
|
+
<p>
|
|
101
|
+
Once the client hydrates, the real URL from the Navigation API takes
|
|
102
|
+
over and <code>ssrPathname</code> is ignored.
|
|
103
|
+
</p>
|
|
104
|
+
<CodeBlock language="tsx">{`const routes = [
|
|
105
|
+
route({
|
|
106
|
+
component: AppShell,
|
|
107
|
+
children: [
|
|
108
|
+
route({ path: "/", component: HomePage }), // Matches ssrPathname="/"
|
|
109
|
+
route({ path: "/about", component: AboutPage }),// Matches ssrPathname="/about"
|
|
110
|
+
route({
|
|
111
|
+
path: "/dashboard",
|
|
112
|
+
component: DashboardPage,
|
|
113
|
+
loader: dashboardLoader, // Skipped during SSR (has loader)
|
|
114
|
+
}),
|
|
115
|
+
],
|
|
116
|
+
}),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
// With ssrPathname="/about":
|
|
120
|
+
// - AppShell renders (pathless, no loader) ✓
|
|
121
|
+
// - AboutPage renders (path matches, no loader) ✓
|
|
122
|
+
// - DashboardPage would NOT render even with ssrPathname="/dashboard"
|
|
123
|
+
// because it has a loader`}</CodeBlock>
|
|
124
|
+
<h4>When to use ssrPathname</h4>
|
|
125
|
+
<p>
|
|
126
|
+
Use <code>ssrPathname</code> when your server or static site generator
|
|
127
|
+
knows the URL being rendered and you want to include page-specific
|
|
128
|
+
content in the SSR output. This is particularly useful for:
|
|
129
|
+
</p>
|
|
130
|
+
<ul>
|
|
131
|
+
<li>
|
|
132
|
+
Improving perceived performance by showing page content immediately
|
|
133
|
+
instead of a blank shell
|
|
134
|
+
</li>
|
|
135
|
+
<li>
|
|
136
|
+
SEO — search engine crawlers see the full page content rather
|
|
137
|
+
than just the app shell
|
|
138
|
+
</li>
|
|
139
|
+
<li>
|
|
140
|
+
Static site generation where each page is pre-rendered at a known
|
|
141
|
+
path
|
|
142
|
+
</li>
|
|
143
|
+
</ul>
|
|
144
|
+
<p>
|
|
145
|
+
If your routes have loaders, those routes will still be skipped during
|
|
146
|
+
SSR. The parent route renders as a shell, and the loader content fills
|
|
147
|
+
in after hydration.
|
|
148
|
+
</p>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
76
151
|
<section>
|
|
77
152
|
<h3>Hooks and SSR</h3>
|
|
78
153
|
<p>
|
|
@@ -152,12 +227,16 @@ function HomePage() {
|
|
|
152
227
|
<h3>Key Takeaways</h3>
|
|
153
228
|
<ul>
|
|
154
229
|
<li>
|
|
155
|
-
|
|
156
|
-
|
|
230
|
+
By default, only pathless routes without loaders render during SSR
|
|
231
|
+
(no URL is available on the server)
|
|
232
|
+
</li>
|
|
233
|
+
<li>
|
|
234
|
+
Use <code>ssrPathname</code> to enable path-based route matching
|
|
235
|
+
during SSR for richer server-rendered output
|
|
157
236
|
</li>
|
|
158
237
|
<li>
|
|
159
|
-
|
|
160
|
-
|
|
238
|
+
Routes with loaders are always skipped during SSR, regardless of{" "}
|
|
239
|
+
<code>ssrPathname</code>
|
|
161
240
|
</li>
|
|
162
241
|
<li>
|
|
163
242
|
Pathless routes are ideal for app shell markup (headers, footers,
|
|
@@ -170,8 +249,8 @@ function HomePage() {
|
|
|
170
249
|
app shell
|
|
171
250
|
</li>
|
|
172
251
|
<li>
|
|
173
|
-
|
|
174
|
-
|
|
252
|
+
Once the client hydrates, the real URL from the Navigation API takes
|
|
253
|
+
over
|
|
175
254
|
</li>
|
|
176
255
|
</ul>
|
|
177
256
|
</section>
|
|
@@ -6,7 +6,7 @@ export function LearnTransitionsPage() {
|
|
|
6
6
|
<h2>Controlling Transitions</h2>
|
|
7
7
|
|
|
8
8
|
<p className="page-intro">
|
|
9
|
-
FUNSTACK Router wraps
|
|
9
|
+
FUNSTACK Router wraps navigations in React's{" "}
|
|
10
10
|
<code>startTransition</code>, which means the old UI may stay visible
|
|
11
11
|
while the new route loads. This page explains how this works and how to
|
|
12
12
|
control it.
|
|
@@ -16,11 +16,11 @@ export function LearnTransitionsPage() {
|
|
|
16
16
|
<h3>Navigations as Transitions</h3>
|
|
17
17
|
<p>
|
|
18
18
|
When the user navigates, the Router updates its location state inside{" "}
|
|
19
|
-
<code>startTransition()</code>. This means React treats
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
<code>startTransition()</code>. This means React treats navigations as
|
|
20
|
+
transitions: if an existing Suspense boundary suspends (e.g., a
|
|
21
|
+
component loading data with <code>use()</code>), React keeps the old
|
|
22
|
+
UI visible instead of immediately showing the fallback. This behavior
|
|
23
|
+
is{" "}
|
|
24
24
|
<a href="https://react.dev/reference/react/useTransition#building-a-suspense-enabled-router">
|
|
25
25
|
what React recommends for Suspense-enabled routers
|
|
26
26
|
</a>
|
|
@@ -141,6 +141,80 @@ function UserDetailPage({
|
|
|
141
141
|
transition behavior is usually the better experience.
|
|
142
142
|
</p>
|
|
143
143
|
</section>
|
|
144
|
+
|
|
145
|
+
<section>
|
|
146
|
+
<h3>Sync State Updates Bypass Transitions</h3>
|
|
147
|
+
<p>
|
|
148
|
+
FUNSTACK Router allows you to save state in a navigation entry, which
|
|
149
|
+
is useful for form state or other UI state that should persist when
|
|
150
|
+
the user navigates back and forth. You can update this state using the{" "}
|
|
151
|
+
<code>setState</code> and <code>resetState</code> functions passed to
|
|
152
|
+
route components. These functions use a replace navigation internally,
|
|
153
|
+
so they trigger a transition.
|
|
154
|
+
</p>
|
|
155
|
+
<p>
|
|
156
|
+
In rare cases, you may want to update navigation state without
|
|
157
|
+
triggering a transition. <code>setStateSync</code> and{" "}
|
|
158
|
+
<code>resetStateSync</code> are designed for this purpose. When you
|
|
159
|
+
call them, the Router updates the current history entry using the
|
|
160
|
+
Navigation API's <code>updateCurrentEntry()</code> method, which does
|
|
161
|
+
not trigger a navigation. The Router detects this and applies the
|
|
162
|
+
update synchronously, outside of <code>startTransition</code>. As a
|
|
163
|
+
result:
|
|
164
|
+
</p>
|
|
165
|
+
<ul>
|
|
166
|
+
<li>
|
|
167
|
+
The update is reflected in the UI immediately — there is no
|
|
168
|
+
pending phase.
|
|
169
|
+
</li>
|
|
170
|
+
<li>
|
|
171
|
+
<code>useIsPending()</code> (and the <code>isPending</code> prop)
|
|
172
|
+
will <strong>not</strong> become <code>true</code>.
|
|
173
|
+
</li>
|
|
174
|
+
<li>
|
|
175
|
+
If the update causes a component to suspend, React will show the
|
|
176
|
+
fallback immediately instead of waiting for the transition to end.
|
|
177
|
+
</li>
|
|
178
|
+
</ul>
|
|
179
|
+
<h4>When to Use Sync State Updates</h4>
|
|
180
|
+
<p>
|
|
181
|
+
When it comes to `setState` vs `setStateSync`, you can think in the
|
|
182
|
+
same way as you would with wrapping state updates in `startTransition`
|
|
183
|
+
or not. A general guideline is to{" "}
|
|
184
|
+
<strong>
|
|
185
|
+
just use <code>setState</code> (with transition)
|
|
186
|
+
</strong>{" "}
|
|
187
|
+
when you don't have a specific reason to avoid it. The exception is
|
|
188
|
+
when the state change <em>already happened</em> on the screen and you
|
|
189
|
+
just want to reflect it in the navigation entry state.
|
|
190
|
+
</p>
|
|
191
|
+
<p>
|
|
192
|
+
A typical example of this is a form where you want to save the current
|
|
193
|
+
input value in the navigation state, so that if the user navigates
|
|
194
|
+
away and then back, their input is preserved. In this case, you would
|
|
195
|
+
call <code>setStateSync</code> in the input's <code>onChange</code>{" "}
|
|
196
|
+
handler, because the state update is already reflected in the input's
|
|
197
|
+
value and you don't want to trigger a transition for this:
|
|
198
|
+
</p>
|
|
199
|
+
<CodeBlock language="tsx">{`function MyForm({ state, setStateSync }: { state: State; setStateSync: (state: State) => void }) {
|
|
200
|
+
const [inputValue, setInputValue] = useState(state.inputValue ?? "");
|
|
201
|
+
|
|
202
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
203
|
+
const newValue = e.target.value;
|
|
204
|
+
setInputValue(newValue);
|
|
205
|
+
setStateSync({ inputValue: newValue }); // Save to navigation state without transition
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return <input value={inputValue} onChange={handleChange} />;
|
|
209
|
+
}`}</CodeBlock>
|
|
210
|
+
<p>
|
|
211
|
+
In this example, using <code>setState</code> (with transition) would
|
|
212
|
+
cause the UI to enter a pending state on every keystroke, which would
|
|
213
|
+
be a poor user experience. By using <code>setStateSync</code>, the
|
|
214
|
+
navigation state updates seamlessly without triggering transitions or
|
|
215
|
+
pending states.
|
|
216
|
+
</p>
|
|
217
|
+
</section>
|
|
144
218
|
</div>
|
|
145
219
|
);
|
|
146
220
|
}
|
|
@@ -225,14 +225,24 @@ const productListRoute = routeState<ProductListState>()(
|
|
|
225
225
|
</li>
|
|
226
226
|
<li>
|
|
227
227
|
<code>setState</code> — Navigate to the same URL with new
|
|
228
|
-
state (creates a new history entry)
|
|
228
|
+
state (creates a new history entry). Goes through a React
|
|
229
|
+
transition, so it may set <code>isPending</code> to{" "}
|
|
230
|
+
<code>true</code>.
|
|
229
231
|
</li>
|
|
230
232
|
<li>
|
|
231
233
|
<code>setStateSync</code> — Update state synchronously without
|
|
232
|
-
creating a new history entry
|
|
234
|
+
creating a new history entry. Bypasses React transitions, so{" "}
|
|
235
|
+
<code>isPending</code> stays <code>false</code>.
|
|
233
236
|
</li>
|
|
234
237
|
<li>
|
|
235
238
|
<code>resetState</code> — Clear the navigation state
|
|
239
|
+
asynchronously via replace navigation. Like <code>setState</code>,
|
|
240
|
+
goes through React transitions.
|
|
241
|
+
</li>
|
|
242
|
+
<li>
|
|
243
|
+
<code>resetStateSync</code> — Clear the navigation state
|
|
244
|
+
synchronously. Like <code>setStateSync</code>, bypasses React
|
|
245
|
+
transitions.
|
|
236
246
|
</li>
|
|
237
247
|
</ul>
|
|
238
248
|
|
package/dist/docs/index.md
CHANGED
|
@@ -16,6 +16,6 @@
|
|
|
16
16
|
- [Navigation API](./LearnNavigationApiPage.tsx) - FUNSTACK Router is built on the Navigation API , a modern browser API that provides a unified way to handle navigation. This guide explains the key differences from the older History API and the benefits this brings to your application.
|
|
17
17
|
- [Nested Routes](./LearnNestedRoutesPage.tsx) - Nested routes let you build complex page layouts where parts of the UI persist across navigation while other parts change. Think of a dashboard with a sidebar that stays in place while the main content area updates—that's nested routing in action.
|
|
18
18
|
- [React Server Components](./LearnRscPage.tsx) - FUNSTACK Router is designed to work with React Server Components (RSC). The package provides a dedicated server entry point so that route definitions can live in server modules, keeping client bundle sizes small.
|
|
19
|
-
- [Server-Side Rendering](./LearnSsrPage.tsx) - FUNSTACK Router supports server-side rendering with a two-stage model. During SSR, pathless (layout) routes without loaders render to produce an app shell, while path-based routes and loaders activate only after client hydration.
|
|
20
|
-
- [Controlling Transitions](./LearnTransitionsPage.tsx) - FUNSTACK Router wraps
|
|
19
|
+
- [Server-Side Rendering](./LearnSsrPage.tsx) - FUNSTACK Router supports server-side rendering with a two-stage model. During SSR, pathless (layout) routes without loaders render to produce an app shell, while path-based routes and loaders activate only after client hydration. You can optionally provide an ssrPathname prop to match path-based routes during SSR for richer server-rendered output.
|
|
20
|
+
- [Controlling Transitions](./LearnTransitionsPage.tsx) - FUNSTACK Router wraps navigations in React's startTransition, which means the old UI may stay visible while the new route loads. This page explains how this works and how to control it.
|
|
21
21
|
- [Type Safety](./LearnTypeSafetyPage.tsx) - FUNSTACK Router provides first-class TypeScript support, allowing you to access route params, navigation state, and loader data with full type safety. This guide covers two approaches: receiving typed data through component props (recommended) and accessing it through hooks.
|