@funstack/router 0.0.6 → 0.0.7-alpha.0
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/skill-installer.d.mts +1 -0
- package/dist/bin/skill-installer.mjs +13 -0
- package/dist/bin/skill-installer.mjs.map +1 -0
- package/dist/docs/ApiComponentsPage.tsx +85 -0
- package/dist/docs/ApiHooksPage.tsx +323 -0
- package/dist/docs/ApiTypesPage.tsx +310 -0
- package/dist/docs/ApiUtilitiesPage.tsx +298 -0
- package/dist/docs/GettingStartedPage.tsx +186 -0
- package/dist/docs/LearnNavigationApiPage.tsx +255 -0
- package/dist/docs/LearnNestedRoutesPage.tsx +601 -0
- package/dist/docs/LearnRscPage.tsx +293 -0
- package/dist/docs/LearnSsrPage.tsx +180 -0
- package/dist/docs/LearnTransitionsPage.tsx +146 -0
- package/dist/docs/LearnTypeSafetyPage.tsx +522 -0
- package/dist/docs/index.md +21 -0
- package/dist/index.d.mts +1 -1
- package/dist/{route-Bc8BUlhv.d.mts → route-ClVnhrQD.d.mts} +1 -1
- package/dist/{route-Bc8BUlhv.d.mts.map → route-ClVnhrQD.d.mts.map} +1 -1
- package/dist/server.d.mts +1 -1
- package/package.json +11 -2
- package/skills/funstack-router-knowledge/SKILL.md +21 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function ApiTypesPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="page docs-page api-page">
|
|
6
|
+
<h1>Types</h1>
|
|
7
|
+
<p className="page-intro">
|
|
8
|
+
TypeScript types and interfaces exported by the router.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<article className="api-item">
|
|
12
|
+
<h3>
|
|
13
|
+
<code>RouteComponentProps<TParams, TState></code>
|
|
14
|
+
</h3>
|
|
15
|
+
<p>
|
|
16
|
+
Props type for route components without a loader. Includes navigation
|
|
17
|
+
state management props.
|
|
18
|
+
</p>
|
|
19
|
+
<CodeBlock language="typescript">{`import type { RouteComponentProps } from "@funstack/router";
|
|
20
|
+
|
|
21
|
+
type Props = RouteComponentProps<
|
|
22
|
+
{ userId: string }, // TParams - path parameters
|
|
23
|
+
{ scrollPosition: number } // TState - navigation state type
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
// Equivalent to:
|
|
27
|
+
type Props = {
|
|
28
|
+
params: { userId: string };
|
|
29
|
+
state: { scrollPosition: number } | undefined;
|
|
30
|
+
// Async state update via replace navigation
|
|
31
|
+
setState: (
|
|
32
|
+
state: { scrollPosition: number } |
|
|
33
|
+
((prev: { scrollPosition: number } | undefined) => { scrollPosition: number })
|
|
34
|
+
) => Promise<void>;
|
|
35
|
+
// Sync state update via updateCurrentEntry
|
|
36
|
+
setStateSync: (
|
|
37
|
+
state: { scrollPosition: number } |
|
|
38
|
+
((prev: { scrollPosition: number } | undefined) => { scrollPosition: number })
|
|
39
|
+
) => void;
|
|
40
|
+
resetState: () => void;
|
|
41
|
+
info: unknown; // Ephemeral navigation info
|
|
42
|
+
isPending: boolean; // Whether a navigation transition is pending
|
|
43
|
+
};`}</CodeBlock>
|
|
44
|
+
<p>
|
|
45
|
+
<strong>setState vs setStateSync:</strong>
|
|
46
|
+
</p>
|
|
47
|
+
<ul>
|
|
48
|
+
<li>
|
|
49
|
+
<code>setState</code> - Async method that returns a Promise. Uses
|
|
50
|
+
replace navigation internally, ensuring the state update goes
|
|
51
|
+
through the full navigation cycle.
|
|
52
|
+
</li>
|
|
53
|
+
<li>
|
|
54
|
+
<code>setStateSync</code> - Synchronous method that updates state
|
|
55
|
+
immediately using <code>navigation.updateCurrentEntry()</code>.
|
|
56
|
+
</li>
|
|
57
|
+
</ul>
|
|
58
|
+
</article>
|
|
59
|
+
|
|
60
|
+
<article className="api-item">
|
|
61
|
+
<h3>
|
|
62
|
+
<code>RouteComponentPropsWithData<TParams, TData, TState></code>
|
|
63
|
+
</h3>
|
|
64
|
+
<p>
|
|
65
|
+
Props type for route components with a loader. Extends{" "}
|
|
66
|
+
<code>RouteComponentProps</code> with a <code>data</code> prop.
|
|
67
|
+
</p>
|
|
68
|
+
<CodeBlock language="typescript">{`import type { RouteComponentPropsWithData } from "@funstack/router";
|
|
69
|
+
|
|
70
|
+
type Props = RouteComponentPropsWithData<
|
|
71
|
+
{ userId: string }, // TParams - path parameters
|
|
72
|
+
User, // TData - loader return type
|
|
73
|
+
{ selectedTab: string } // TState - navigation state type
|
|
74
|
+
>;
|
|
75
|
+
|
|
76
|
+
// Equivalent to:
|
|
77
|
+
type Props = {
|
|
78
|
+
params: { userId: string };
|
|
79
|
+
data: User;
|
|
80
|
+
state: { selectedTab: string } | undefined;
|
|
81
|
+
setState: (state: ...) => Promise<void>; // async
|
|
82
|
+
setStateSync: (state: ...) => void; // sync
|
|
83
|
+
resetState: () => void;
|
|
84
|
+
info: unknown; // Ephemeral navigation info
|
|
85
|
+
isPending: boolean; // Whether a navigation transition is pending
|
|
86
|
+
};`}</CodeBlock>
|
|
87
|
+
</article>
|
|
88
|
+
|
|
89
|
+
<article className="api-item">
|
|
90
|
+
<h3>
|
|
91
|
+
<code>PathParams<T></code>
|
|
92
|
+
</h3>
|
|
93
|
+
<p>
|
|
94
|
+
Utility type that extracts parameter types from a path pattern string.
|
|
95
|
+
</p>
|
|
96
|
+
<CodeBlock language="tsx">{`import type { PathParams } from "@funstack/router";
|
|
97
|
+
|
|
98
|
+
// PathParams<"/users/:userId"> = { userId: string }
|
|
99
|
+
// PathParams<"/users/:userId/posts/:postId"> = { userId: string; postId: string }
|
|
100
|
+
// PathParams<"/about"> = Record<string, never>
|
|
101
|
+
|
|
102
|
+
type MyParams = PathParams<"/users/:userId">;
|
|
103
|
+
// { userId: string }`}</CodeBlock>
|
|
104
|
+
</article>
|
|
105
|
+
|
|
106
|
+
<article className="api-item">
|
|
107
|
+
<h3>
|
|
108
|
+
<code>
|
|
109
|
+
TypefulOpaqueRouteDefinition<Id, Params, State, Data>
|
|
110
|
+
</code>
|
|
111
|
+
</h3>
|
|
112
|
+
<p>
|
|
113
|
+
A route definition that carries type information. Created when using{" "}
|
|
114
|
+
<code>route()</code> or <code>routeState()</code> with an{" "}
|
|
115
|
+
<code>id</code> property. This enables type-safe access to route
|
|
116
|
+
params, state, and data via hooks.
|
|
117
|
+
</p>
|
|
118
|
+
<CodeBlock language="tsx">{`import { route, routeState } from "@funstack/router";
|
|
119
|
+
import type { TypefulOpaqueRouteDefinition } from "@funstack/router";
|
|
120
|
+
|
|
121
|
+
// Route with id gets TypefulOpaqueRouteDefinition type
|
|
122
|
+
const userRoute = route({
|
|
123
|
+
id: "user",
|
|
124
|
+
path: "/users/:userId",
|
|
125
|
+
loader: () => ({ name: "John" }),
|
|
126
|
+
component: UserPage,
|
|
127
|
+
});
|
|
128
|
+
// Type: TypefulOpaqueRouteDefinition<"user", { userId: string }, undefined, { name: string }>
|
|
129
|
+
|
|
130
|
+
// Route without id gets OpaqueRouteDefinition (no type info)
|
|
131
|
+
const aboutRoute = route({
|
|
132
|
+
path: "/about",
|
|
133
|
+
component: AboutPage,
|
|
134
|
+
});
|
|
135
|
+
// Type: OpaqueRouteDefinition`}</CodeBlock>
|
|
136
|
+
</article>
|
|
137
|
+
|
|
138
|
+
<article className="api-item">
|
|
139
|
+
<h3>Type Extraction Utilities</h3>
|
|
140
|
+
<p>
|
|
141
|
+
Helper types to extract type information from{" "}
|
|
142
|
+
<code>TypefulOpaqueRouteDefinition</code>. Useful for advanced type
|
|
143
|
+
manipulation.
|
|
144
|
+
</p>
|
|
145
|
+
<CodeBlock language="tsx">{`import type {
|
|
146
|
+
ExtractRouteId,
|
|
147
|
+
ExtractRouteParams,
|
|
148
|
+
ExtractRouteState,
|
|
149
|
+
ExtractRouteData,
|
|
150
|
+
} from "@funstack/router";
|
|
151
|
+
|
|
152
|
+
const userRoute = route({
|
|
153
|
+
id: "user",
|
|
154
|
+
path: "/users/:userId",
|
|
155
|
+
loader: () => ({ name: "John", age: 30 }),
|
|
156
|
+
component: UserPage,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
type Id = ExtractRouteId<typeof userRoute>;
|
|
160
|
+
// "user"
|
|
161
|
+
|
|
162
|
+
type Params = ExtractRouteParams<typeof userRoute>;
|
|
163
|
+
// { userId: string }
|
|
164
|
+
|
|
165
|
+
type State = ExtractRouteState<typeof userRoute>;
|
|
166
|
+
// undefined
|
|
167
|
+
|
|
168
|
+
type Data = ExtractRouteData<typeof userRoute>;
|
|
169
|
+
// { name: string; age: number }`}</CodeBlock>
|
|
170
|
+
</article>
|
|
171
|
+
|
|
172
|
+
<article className="api-item">
|
|
173
|
+
<h3>
|
|
174
|
+
<code>RouteComponentPropsOf<T></code>
|
|
175
|
+
</h3>
|
|
176
|
+
<p>
|
|
177
|
+
Utility type that extracts the component props type from a route
|
|
178
|
+
definition. Returns <code>RouteComponentProps</code> for routes
|
|
179
|
+
without a loader, or <code>RouteComponentPropsWithData</code> for
|
|
180
|
+
routes with a loader. This is useful for typing route components
|
|
181
|
+
separately from the route definition.
|
|
182
|
+
</p>
|
|
183
|
+
<CodeBlock language="tsx">{`import { route, routeState } from "@funstack/router";
|
|
184
|
+
import type { RouteComponentPropsOf } from "@funstack/router";
|
|
185
|
+
|
|
186
|
+
// Route without loader
|
|
187
|
+
const userRoute = route({
|
|
188
|
+
id: "user",
|
|
189
|
+
path: "/users/:userId",
|
|
190
|
+
component: UserPage,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
type UserPageProps = RouteComponentPropsOf<typeof userRoute>;
|
|
194
|
+
// RouteComponentProps<{ userId: string }, undefined>
|
|
195
|
+
|
|
196
|
+
function UserPage({ params }: UserPageProps) {
|
|
197
|
+
return <h1>User: {params.userId}</h1>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Route with loader
|
|
201
|
+
const profileRoute = route({
|
|
202
|
+
id: "profile",
|
|
203
|
+
path: "/profile/:userId",
|
|
204
|
+
loader: () => ({ name: "John", age: 30 }),
|
|
205
|
+
component: ProfilePage,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
type ProfilePageProps = RouteComponentPropsOf<typeof profileRoute>;
|
|
209
|
+
// RouteComponentPropsWithData<{ userId: string }, { name: string; age: number }, undefined>
|
|
210
|
+
|
|
211
|
+
// Route with state
|
|
212
|
+
type MyState = { tab: string };
|
|
213
|
+
const settingsRoute = routeState<MyState>()({
|
|
214
|
+
id: "settings",
|
|
215
|
+
path: "/settings",
|
|
216
|
+
component: SettingsPage,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
type SettingsPageProps = RouteComponentPropsOf<typeof settingsRoute>;
|
|
220
|
+
// RouteComponentProps<Record<string, never>, MyState>`}</CodeBlock>
|
|
221
|
+
<p>
|
|
222
|
+
<strong>Note:</strong> This utility requires a route with an{" "}
|
|
223
|
+
<code>id</code> property. Using it with a route without{" "}
|
|
224
|
+
<code>id</code> will result in a type error.
|
|
225
|
+
</p>
|
|
226
|
+
</article>
|
|
227
|
+
|
|
228
|
+
<article className="api-item">
|
|
229
|
+
<h3>
|
|
230
|
+
<code>RouteDefinition</code>
|
|
231
|
+
</h3>
|
|
232
|
+
<p>
|
|
233
|
+
The <code>component</code> field accepts two forms:
|
|
234
|
+
</p>
|
|
235
|
+
<ul>
|
|
236
|
+
<li>
|
|
237
|
+
<strong>Component reference</strong> (e.g.,{" "}
|
|
238
|
+
<code>component: UserPage</code>): Router automatically injects
|
|
239
|
+
props (<code>params</code>, <code>state</code>,{" "}
|
|
240
|
+
<code>setState</code>, <code>setStateSync</code>,{" "}
|
|
241
|
+
<code>resetState</code>, <code>info</code>, <code>isPending</code>,
|
|
242
|
+
and <code>data</code> when a loader is defined).
|
|
243
|
+
</li>
|
|
244
|
+
<li>
|
|
245
|
+
<strong>JSX element</strong> (e.g.,{" "}
|
|
246
|
+
<code>component: <UserPage /></code>): Rendered as-is without
|
|
247
|
+
router props injection. Useful for static components or when you
|
|
248
|
+
want to pass custom props.
|
|
249
|
+
</li>
|
|
250
|
+
</ul>
|
|
251
|
+
<CodeBlock language="tsx">{`// Component reference: router injects props automatically
|
|
252
|
+
route({
|
|
253
|
+
path: "/users/:userId",
|
|
254
|
+
component: UserPage, // receives { params, state, setState, ... }
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// JSX element: rendered as-is, no props injection
|
|
258
|
+
route({
|
|
259
|
+
path: "/about",
|
|
260
|
+
component: <AboutPage title="About Us" />, // custom props only
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// With loader and state:
|
|
264
|
+
routeState<{ tab: string }>()({
|
|
265
|
+
path: "/users/:userId",
|
|
266
|
+
component: UserPage, // receives { data, params, state, ... }
|
|
267
|
+
loader: () => fetchUser(),
|
|
268
|
+
});`}</CodeBlock>
|
|
269
|
+
</article>
|
|
270
|
+
|
|
271
|
+
<article className="api-item">
|
|
272
|
+
<h3>
|
|
273
|
+
<code>LoaderArgs</code>
|
|
274
|
+
</h3>
|
|
275
|
+
<CodeBlock language="typescript">{`interface LoaderArgs {
|
|
276
|
+
params: Record<string, string>;
|
|
277
|
+
request: Request;
|
|
278
|
+
signal: AbortSignal;
|
|
279
|
+
}`}</CodeBlock>
|
|
280
|
+
</article>
|
|
281
|
+
|
|
282
|
+
<article className="api-item">
|
|
283
|
+
<h3>
|
|
284
|
+
<code>Location</code>
|
|
285
|
+
</h3>
|
|
286
|
+
<CodeBlock language="typescript">{`interface Location {
|
|
287
|
+
pathname: string;
|
|
288
|
+
search: string;
|
|
289
|
+
hash: string;
|
|
290
|
+
}`}</CodeBlock>
|
|
291
|
+
</article>
|
|
292
|
+
|
|
293
|
+
<article className="api-item">
|
|
294
|
+
<h3>
|
|
295
|
+
<code>NavigateOptions</code>
|
|
296
|
+
</h3>
|
|
297
|
+
<CodeBlock language="typescript">{`interface NavigateOptions {
|
|
298
|
+
replace?: boolean;
|
|
299
|
+
state?: unknown;
|
|
300
|
+
info?: unknown; // Ephemeral, not persisted in history
|
|
301
|
+
}`}</CodeBlock>
|
|
302
|
+
<p>
|
|
303
|
+
<strong>Note:</strong> <code>state</code> is persisted in history and
|
|
304
|
+
available across back/forward navigation. <code>info</code> is
|
|
305
|
+
ephemeral and only available during the navigation that triggered it.
|
|
306
|
+
</p>
|
|
307
|
+
</article>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function ApiUtilitiesPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="page docs-page api-page">
|
|
6
|
+
<h1>Utilities</h1>
|
|
7
|
+
<p className="page-intro">
|
|
8
|
+
Helper functions for defining routes and managing state.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<article className="api-item">
|
|
12
|
+
<h3>
|
|
13
|
+
<code>route()</code>
|
|
14
|
+
</h3>
|
|
15
|
+
<p>
|
|
16
|
+
Helper function to define routes with proper typing. The component
|
|
17
|
+
always receives a <code>params</code> prop with types inferred from
|
|
18
|
+
the path pattern. When a <code>loader</code> is defined, the component
|
|
19
|
+
also receives a <code>data</code> prop. Components also receive{" "}
|
|
20
|
+
<code>state</code>, <code>setState</code>, <code>setStateSync</code>,
|
|
21
|
+
and <code>resetState</code> props for navigation state management.
|
|
22
|
+
</p>
|
|
23
|
+
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
24
|
+
|
|
25
|
+
// Route without loader - component receives params prop
|
|
26
|
+
function ProfileTab({ params }: { params: { userId: string } }) {
|
|
27
|
+
return <div>Profile for user {params.userId}</div>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Route with loader - component receives both data and params props
|
|
31
|
+
function UserPage({
|
|
32
|
+
data,
|
|
33
|
+
params,
|
|
34
|
+
}: {
|
|
35
|
+
data: User;
|
|
36
|
+
params: { userId: string };
|
|
37
|
+
}) {
|
|
38
|
+
return <h1>{data.name} (ID: {params.userId})</h1>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const myRoute = route({
|
|
42
|
+
path: "/users/:userId",
|
|
43
|
+
component: UserPage,
|
|
44
|
+
loader: async ({ params }) => {
|
|
45
|
+
return fetchUser(params.userId);
|
|
46
|
+
},
|
|
47
|
+
children: [
|
|
48
|
+
route({ path: "/profile", component: ProfileTab }),
|
|
49
|
+
route({ path: "/settings", component: SettingsTab }),
|
|
50
|
+
],
|
|
51
|
+
});`}</CodeBlock>
|
|
52
|
+
<h4>Options</h4>
|
|
53
|
+
<table className="props-table">
|
|
54
|
+
<thead>
|
|
55
|
+
<tr>
|
|
56
|
+
<th>Option</th>
|
|
57
|
+
<th>Type</th>
|
|
58
|
+
<th>Description</th>
|
|
59
|
+
</tr>
|
|
60
|
+
</thead>
|
|
61
|
+
<tbody>
|
|
62
|
+
<tr>
|
|
63
|
+
<td>
|
|
64
|
+
<code>path</code>
|
|
65
|
+
</td>
|
|
66
|
+
<td>
|
|
67
|
+
<code>string</code> (optional)
|
|
68
|
+
</td>
|
|
69
|
+
<td>
|
|
70
|
+
URL path pattern (supports <code>:param</code> syntax). If
|
|
71
|
+
omitted, creates a pathless route that always matches and
|
|
72
|
+
consumes no pathname. Useful for layout wrappers.
|
|
73
|
+
</td>
|
|
74
|
+
</tr>
|
|
75
|
+
<tr>
|
|
76
|
+
<td>
|
|
77
|
+
<code>component</code>
|
|
78
|
+
</td>
|
|
79
|
+
<td>
|
|
80
|
+
<code>ComponentType</code>
|
|
81
|
+
</td>
|
|
82
|
+
<td>
|
|
83
|
+
React component to render. Receives <code>params</code> prop
|
|
84
|
+
(and <code>data</code> prop if loader is defined)
|
|
85
|
+
</td>
|
|
86
|
+
</tr>
|
|
87
|
+
<tr>
|
|
88
|
+
<td>
|
|
89
|
+
<code>loader</code>
|
|
90
|
+
</td>
|
|
91
|
+
<td>
|
|
92
|
+
<code>(args: LoaderArgs) => T</code>
|
|
93
|
+
</td>
|
|
94
|
+
<td>Function to load data. May be synchronous or asynchronous</td>
|
|
95
|
+
</tr>
|
|
96
|
+
<tr>
|
|
97
|
+
<td>
|
|
98
|
+
<code>children</code>
|
|
99
|
+
</td>
|
|
100
|
+
<td>
|
|
101
|
+
<code>RouteDefinition[]</code>
|
|
102
|
+
</td>
|
|
103
|
+
<td>Nested child routes</td>
|
|
104
|
+
</tr>
|
|
105
|
+
<tr>
|
|
106
|
+
<td>
|
|
107
|
+
<code>exact</code>
|
|
108
|
+
</td>
|
|
109
|
+
<td>
|
|
110
|
+
<code>boolean</code>
|
|
111
|
+
</td>
|
|
112
|
+
<td>
|
|
113
|
+
Override default matching. <code>true</code> = exact match only,{" "}
|
|
114
|
+
<code>false</code> = prefix match. Defaults to <code>true</code>{" "}
|
|
115
|
+
for leaf routes, <code>false</code> for parent routes.
|
|
116
|
+
</td>
|
|
117
|
+
</tr>
|
|
118
|
+
<tr>
|
|
119
|
+
<td>
|
|
120
|
+
<code>requireChildren</code>
|
|
121
|
+
</td>
|
|
122
|
+
<td>
|
|
123
|
+
<code>boolean</code>
|
|
124
|
+
</td>
|
|
125
|
+
<td>
|
|
126
|
+
Whether a parent route requires a child to match.{" "}
|
|
127
|
+
<code>true</code> (default) = parent only matches if a child
|
|
128
|
+
matches, <code>false</code> = parent can match alone with{" "}
|
|
129
|
+
<code>outlet</code> as <code>null</code>. Enables catch-all
|
|
130
|
+
routes to work intuitively.
|
|
131
|
+
</td>
|
|
132
|
+
</tr>
|
|
133
|
+
</tbody>
|
|
134
|
+
</table>
|
|
135
|
+
</article>
|
|
136
|
+
|
|
137
|
+
<article className="api-item">
|
|
138
|
+
<h3>
|
|
139
|
+
<code>routeState<TState>()</code>
|
|
140
|
+
</h3>
|
|
141
|
+
<p>
|
|
142
|
+
Curried helper function for defining routes with typed navigation
|
|
143
|
+
state. Use this when your route component needs to manage state that
|
|
144
|
+
persists across browser back/forward navigation.
|
|
145
|
+
</p>
|
|
146
|
+
<CodeBlock language="tsx">{`import { routeState, type RouteComponentProps } from "@funstack/router";
|
|
147
|
+
|
|
148
|
+
type PageState = { scrollPosition: number; selectedTab: string };
|
|
149
|
+
|
|
150
|
+
function UserPage({
|
|
151
|
+
params,
|
|
152
|
+
state,
|
|
153
|
+
setState,
|
|
154
|
+
setStateSync,
|
|
155
|
+
resetState,
|
|
156
|
+
}: RouteComponentProps<{ userId: string }, PageState>) {
|
|
157
|
+
// state is PageState | undefined (undefined on first visit)
|
|
158
|
+
const scrollPosition = state?.scrollPosition ?? 0;
|
|
159
|
+
const selectedTab = state?.selectedTab ?? "posts";
|
|
160
|
+
|
|
161
|
+
// Async state update (recommended for most cases)
|
|
162
|
+
const handleTabChange = async (tab: string) => {
|
|
163
|
+
await setState({ scrollPosition, selectedTab: tab });
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Sync state update (for immediate updates like scroll position)
|
|
167
|
+
const handleScroll = (position: number) => {
|
|
168
|
+
setStateSync({ scrollPosition: position, selectedTab });
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div>
|
|
173
|
+
<h1>User {params.userId}</h1>
|
|
174
|
+
<button onClick={() => resetState()}>Clear State</button>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Use routeState<TState>() for typed state management
|
|
180
|
+
const userRoute = routeState<PageState>()({
|
|
181
|
+
path: "/users/:userId",
|
|
182
|
+
component: UserPage,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// With loader
|
|
186
|
+
const productRoute = routeState<{ filter: string }>()({
|
|
187
|
+
path: "/products",
|
|
188
|
+
component: ProductList,
|
|
189
|
+
loader: async () => fetchProducts(),
|
|
190
|
+
});`}</CodeBlock>
|
|
191
|
+
<h4>How It Works</h4>
|
|
192
|
+
<p>
|
|
193
|
+
Navigation state is stored in the browser's{" "}
|
|
194
|
+
<a
|
|
195
|
+
href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API"
|
|
196
|
+
target="_blank"
|
|
197
|
+
rel="noopener noreferrer"
|
|
198
|
+
>
|
|
199
|
+
Navigation API
|
|
200
|
+
</a>
|
|
201
|
+
. The router provides two methods to update state:
|
|
202
|
+
</p>
|
|
203
|
+
<ul>
|
|
204
|
+
<li>
|
|
205
|
+
<code>setState</code> - Async method that uses replace navigation.
|
|
206
|
+
Returns a Promise that resolves when the navigation completes.
|
|
207
|
+
</li>
|
|
208
|
+
<li>
|
|
209
|
+
<code>setStateSync</code> - Sync method that uses{" "}
|
|
210
|
+
<code>navigation.updateCurrentEntry()</code>. Updates state
|
|
211
|
+
immediately without waiting.
|
|
212
|
+
</li>
|
|
213
|
+
</ul>
|
|
214
|
+
<p>Navigation state characteristics:</p>
|
|
215
|
+
<ul>
|
|
216
|
+
<li>State persists when navigating back/forward in history</li>
|
|
217
|
+
<li>Each history entry has its own independent state</li>
|
|
218
|
+
<li>
|
|
219
|
+
State must be serializable (no functions, Symbols, or DOM nodes)
|
|
220
|
+
</li>
|
|
221
|
+
</ul>
|
|
222
|
+
<h4>Internal Storage</h4>
|
|
223
|
+
<p>
|
|
224
|
+
The router stores state internally using a <code>__routeStates</code>{" "}
|
|
225
|
+
array indexed by route match position. This enables each nested route
|
|
226
|
+
to maintain independent state:
|
|
227
|
+
</p>
|
|
228
|
+
<CodeBlock language="typescript">{`// Internal structure stored in NavigationHistoryEntry
|
|
229
|
+
{
|
|
230
|
+
__routeStates: [
|
|
231
|
+
{ sidebarOpen: true }, // Layout (index 0)
|
|
232
|
+
{ selectedTab: "posts" }, // UserPage (index 1)
|
|
233
|
+
{ scrollY: 500 }, // PostsPage (index 2)
|
|
234
|
+
]
|
|
235
|
+
}`}</CodeBlock>
|
|
236
|
+
</article>
|
|
237
|
+
|
|
238
|
+
<article className="api-item">
|
|
239
|
+
<h3>Server Entry Point</h3>
|
|
240
|
+
<p>
|
|
241
|
+
The <code>route()</code> and <code>routeState()</code> helpers are
|
|
242
|
+
also available from a server-compatible entry point. Use this when
|
|
243
|
+
defining routes in React Server Components or other server-side code.
|
|
244
|
+
</p>
|
|
245
|
+
<CodeBlock language="tsx">{`// In Server Components or server-side route definitions
|
|
246
|
+
import { route, routeState } from "@funstack/router/server";
|
|
247
|
+
|
|
248
|
+
// Define routes without the "use client" directive
|
|
249
|
+
const routes = [
|
|
250
|
+
route({
|
|
251
|
+
path: "/",
|
|
252
|
+
component: HomePage,
|
|
253
|
+
}),
|
|
254
|
+
routeState<{ tab: string }>()({
|
|
255
|
+
path: "/dashboard",
|
|
256
|
+
component: DashboardPage,
|
|
257
|
+
}),
|
|
258
|
+
];`}</CodeBlock>
|
|
259
|
+
<h4>When to Use</h4>
|
|
260
|
+
<ul>
|
|
261
|
+
<li>Defining routes in React Server Components</li>
|
|
262
|
+
<li>Server-side route configuration files</li>
|
|
263
|
+
<li>
|
|
264
|
+
Any context where <code>"use client"</code> would cause issues
|
|
265
|
+
</li>
|
|
266
|
+
</ul>
|
|
267
|
+
<h4>Available Exports</h4>
|
|
268
|
+
<p>
|
|
269
|
+
The <code>@funstack/router/server</code> entry point exports:
|
|
270
|
+
</p>
|
|
271
|
+
<ul>
|
|
272
|
+
<li>
|
|
273
|
+
<code>route</code> - Route definition helper
|
|
274
|
+
</li>
|
|
275
|
+
<li>
|
|
276
|
+
<code>routeState</code> - Route definition helper with typed state
|
|
277
|
+
</li>
|
|
278
|
+
<li>
|
|
279
|
+
Types: <code>LoaderArgs</code>, <code>RouteDefinition</code>,{" "}
|
|
280
|
+
<code>PathParams</code>, <code>RouteComponentProps</code>,{" "}
|
|
281
|
+
<code>RouteComponentPropsWithData</code>
|
|
282
|
+
</li>
|
|
283
|
+
</ul>
|
|
284
|
+
<p>
|
|
285
|
+
For client-side features like <code>Router</code>, <code>Outlet</code>
|
|
286
|
+
, and hooks, use the main <code>@funstack/router</code> entry point.
|
|
287
|
+
</p>
|
|
288
|
+
<p>
|
|
289
|
+
See the{" "}
|
|
290
|
+
<a href="/funstack-router/learn/react-server-components">
|
|
291
|
+
React Server Components
|
|
292
|
+
</a>{" "}
|
|
293
|
+
guide for a full walkthrough of using the server entry point.
|
|
294
|
+
</p>
|
|
295
|
+
</article>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|