@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.
@@ -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&lt;TParams, TState&gt;</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&lt;TParams, TData, TState&gt;</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&lt;T&gt;</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&lt;Id, Params, State, Data&gt;
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&lt;T&gt;</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: &lt;UserPage /&gt;</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) =&gt; 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&lt;TState&gt;()</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
+ }