@funstack/router 0.0.10 → 1.0.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/{route-DRcgs0Pt.d.mts → bindRoute-BtT4qPKI.d.mts} +169 -12
- package/dist/bindRoute-BtT4qPKI.d.mts.map +1 -0
- package/dist/{route-p_gr5yPI.mjs → bindRoute-C7JBYje-.mjs} +11 -2
- package/dist/bindRoute-C7JBYje-.mjs.map +1 -0
- package/dist/docs/ApiHooksPage.tsx +0 -22
- package/dist/docs/ApiUtilitiesPage.tsx +62 -3
- package/dist/docs/ExamplesPage.tsx +3 -5
- package/dist/docs/FaqPage.tsx +84 -0
- package/dist/docs/GettingStartedPage.tsx +6 -3
- package/dist/docs/LearnActionsPage.tsx +228 -0
- package/dist/docs/LearnNavigationApiPage.tsx +1 -1
- package/dist/docs/LearnRouteDefinitionsPage.tsx +285 -0
- package/dist/docs/LearnRscPage.tsx +6 -0
- package/dist/docs/LearnSsgPage.tsx +3 -5
- package/dist/docs/index.md +3 -0
- package/dist/index.d.mts +16 -11
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +51 -39
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +2 -2
- package/dist/server.mjs +2 -2
- package/package.json +4 -4
- package/skills/funstack-router-knowledge/SKILL.md +1 -1
- package/dist/route-DRcgs0Pt.d.mts.map +0 -1
- package/dist/route-p_gr5yPI.mjs.map +0 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function LearnActionsPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="learn-content">
|
|
6
|
+
<h2>Form Actions</h2>
|
|
7
|
+
|
|
8
|
+
<p className="page-intro">
|
|
9
|
+
FUNSTACK Router can intercept <code>{"<form>"}</code> submissions and
|
|
10
|
+
run an <strong>action</strong> function on the client before navigation
|
|
11
|
+
occurs. This guide explains how actions work, when to use them, and
|
|
12
|
+
important considerations for progressive enhancement.
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
<div className="callout warning">
|
|
16
|
+
<p className="callout-title">Important: Progressive Enhancement</p>
|
|
17
|
+
<p>
|
|
18
|
+
A <code>{'<form method="post">'}</code> should work even{" "}
|
|
19
|
+
<strong>before JavaScript has loaded</strong>. The browser natively
|
|
20
|
+
submits POST forms to the server, so your server must be prepared to
|
|
21
|
+
handle these requests. The router’s <code>action</code> function
|
|
22
|
+
is a <strong>client-side shortcut</strong> that runs only after
|
|
23
|
+
hydration — it does not replace server-side form handling.
|
|
24
|
+
</p>
|
|
25
|
+
<p>
|
|
26
|
+
If your server cannot handle the POST request, users on slow
|
|
27
|
+
connections, users with JavaScript disabled, or users who submit the
|
|
28
|
+
form before hydration completes will experience a broken form. Always
|
|
29
|
+
ensure your server handles POST submissions for the same URL as a
|
|
30
|
+
baseline.
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<section>
|
|
35
|
+
<h3>How It Works</h3>
|
|
36
|
+
<p>
|
|
37
|
+
When a <code>{'<form method="post">'}</code> is submitted, the router
|
|
38
|
+
matches the form’s destination URL against the route
|
|
39
|
+
definitions. If a matched route defines an <code>action</code>, the
|
|
40
|
+
router intercepts the submission via the Navigation API instead of
|
|
41
|
+
letting the browser send it to the server. The flow is:
|
|
42
|
+
</p>
|
|
43
|
+
<ol>
|
|
44
|
+
<li>
|
|
45
|
+
User submits a form with <code>method="post"</code>
|
|
46
|
+
</li>
|
|
47
|
+
<li>
|
|
48
|
+
The Navigation API fires a <code>navigate</code> event with{" "}
|
|
49
|
+
<code>formData</code>
|
|
50
|
+
</li>
|
|
51
|
+
<li>
|
|
52
|
+
The router finds the deepest matched route that has an{" "}
|
|
53
|
+
<code>action</code>
|
|
54
|
+
</li>
|
|
55
|
+
<li>
|
|
56
|
+
The <code>action</code> function runs with the form data wrapped in
|
|
57
|
+
a <code>Request</code>
|
|
58
|
+
</li>
|
|
59
|
+
<li>
|
|
60
|
+
The action’s return value is passed to the route’s{" "}
|
|
61
|
+
<code>loader</code> as <code>actionResult</code>
|
|
62
|
+
</li>
|
|
63
|
+
<li>The loader runs and the UI updates with fresh data</li>
|
|
64
|
+
</ol>
|
|
65
|
+
<p>
|
|
66
|
+
If the matched route does <strong>not</strong> define an action, the
|
|
67
|
+
router does not intercept the submission and the browser sends it to
|
|
68
|
+
the server as a normal POST request.
|
|
69
|
+
</p>
|
|
70
|
+
</section>
|
|
71
|
+
|
|
72
|
+
<section>
|
|
73
|
+
<h3>Defining an Action</h3>
|
|
74
|
+
<p>
|
|
75
|
+
Add an <code>action</code> function to your route definition. It
|
|
76
|
+
receives an <code>ActionArgs</code> object with the route params, a{" "}
|
|
77
|
+
<code>Request</code>, and an <code>AbortSignal</code>:
|
|
78
|
+
</p>
|
|
79
|
+
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
80
|
+
|
|
81
|
+
const editRoute = route({
|
|
82
|
+
path: "/posts/:postId/edit",
|
|
83
|
+
action: async ({ params, request, signal }) => {
|
|
84
|
+
const formData = await request.formData();
|
|
85
|
+
const title = formData.get("title") as string;
|
|
86
|
+
const body = formData.get("body") as string;
|
|
87
|
+
|
|
88
|
+
const res = await fetch(\`/api/posts/\${params.postId}\`, {
|
|
89
|
+
method: "PUT",
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
body: JSON.stringify({ title, body }),
|
|
92
|
+
signal,
|
|
93
|
+
});
|
|
94
|
+
return res.json();
|
|
95
|
+
},
|
|
96
|
+
loader: async ({ params, actionResult, signal }) => {
|
|
97
|
+
// After a successful action, actionResult contains
|
|
98
|
+
// the return value. On normal navigations it is undefined.
|
|
99
|
+
const res = await fetch(\`/api/posts/\${params.postId}\`, { signal });
|
|
100
|
+
return res.json();
|
|
101
|
+
},
|
|
102
|
+
component: EditPostPage,
|
|
103
|
+
});`}</CodeBlock>
|
|
104
|
+
</section>
|
|
105
|
+
|
|
106
|
+
<section>
|
|
107
|
+
<h3>The Form</h3>
|
|
108
|
+
<p>
|
|
109
|
+
Use a standard HTML <code>{"<form>"}</code> element with{" "}
|
|
110
|
+
<code>method="post"</code>. There is no special form component needed
|
|
111
|
+
— the router hooks into the Navigation API which intercepts
|
|
112
|
+
native form submissions:
|
|
113
|
+
</p>
|
|
114
|
+
<CodeBlock language="tsx">{`function EditPostPage({ data, params }: EditPostProps) {
|
|
115
|
+
return (
|
|
116
|
+
<form method="post" action={\`/posts/\${params.postId}/edit\`}>
|
|
117
|
+
<input name="title" defaultValue={data.title} />
|
|
118
|
+
<textarea name="body" defaultValue={data.body} />
|
|
119
|
+
<button type="submit">Save</button>
|
|
120
|
+
</form>
|
|
121
|
+
);
|
|
122
|
+
}`}</CodeBlock>
|
|
123
|
+
<p>
|
|
124
|
+
Note that the form’s <code>action</code> attribute points to the
|
|
125
|
+
same URL that the route matches. This is essential for progressive
|
|
126
|
+
enhancement: before hydration, the browser will POST to this URL on
|
|
127
|
+
the server.
|
|
128
|
+
</p>
|
|
129
|
+
</section>
|
|
130
|
+
|
|
131
|
+
<section>
|
|
132
|
+
<h3>Progressive Enhancement in Detail</h3>
|
|
133
|
+
<p>
|
|
134
|
+
The action feature is designed as an{" "}
|
|
135
|
+
<strong>enhancement layer</strong>. The baseline behavior of a POST
|
|
136
|
+
form is a server round-trip, and the router’s action provides a
|
|
137
|
+
faster, client-side alternative once hydration is complete. This
|
|
138
|
+
means:
|
|
139
|
+
</p>
|
|
140
|
+
<ul>
|
|
141
|
+
<li>
|
|
142
|
+
<strong>Before hydration</strong> — The browser submits the
|
|
143
|
+
form to the server as a normal POST request. Your server must handle
|
|
144
|
+
it and return an appropriate response (typically a redirect or a
|
|
145
|
+
re-rendered page).
|
|
146
|
+
</li>
|
|
147
|
+
<li>
|
|
148
|
+
<strong>After hydration</strong> — The router intercepts the
|
|
149
|
+
submission, runs your <code>action</code> function on the client,
|
|
150
|
+
and updates the UI without a full page reload.
|
|
151
|
+
</li>
|
|
152
|
+
</ul>
|
|
153
|
+
<p>
|
|
154
|
+
Both paths should produce the same end result for the user. The client
|
|
155
|
+
action is a shortcut, not a replacement.
|
|
156
|
+
</p>
|
|
157
|
+
|
|
158
|
+
<div className="callout warning">
|
|
159
|
+
<p className="callout-title">
|
|
160
|
+
When your server cannot handle POST requests
|
|
161
|
+
</p>
|
|
162
|
+
<p>
|
|
163
|
+
If you are building a purely client-side application (e.g. a SPA
|
|
164
|
+
with no server-side form handling), consider using React 19’s{" "}
|
|
165
|
+
<code>{"<form action={fn}>"}</code> pattern instead. When a form
|
|
166
|
+
action is a <strong>function</strong> rather than a URL, the browser
|
|
167
|
+
will not attempt a server round-trip on submission. Note that in a
|
|
168
|
+
client-only app the form will not work until React hydrates, since
|
|
169
|
+
the function only exists in the JavaScript bundle.
|
|
170
|
+
</p>
|
|
171
|
+
<p>
|
|
172
|
+
In contrast, FUNSTACK Router’s <code>action</code> intercepts
|
|
173
|
+
URL-based form submissions. If the client has not hydrated yet, the
|
|
174
|
+
browser will POST to the URL, which will fail without server
|
|
175
|
+
handling.
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
</section>
|
|
179
|
+
|
|
180
|
+
<section>
|
|
181
|
+
<h3>Action Result and Loader</h3>
|
|
182
|
+
<p>
|
|
183
|
+
When a route defines both an <code>action</code> and a{" "}
|
|
184
|
+
<code>loader</code>, the loader runs after the action completes. The
|
|
185
|
+
action’s return value is passed to the loader via the{" "}
|
|
186
|
+
<code>actionResult</code> parameter:
|
|
187
|
+
</p>
|
|
188
|
+
<CodeBlock language="typescript">{`action: async ({ request }) => {
|
|
189
|
+
const formData = await request.formData();
|
|
190
|
+
// ... process form
|
|
191
|
+
return { success: true, message: "Saved!" };
|
|
192
|
+
},
|
|
193
|
+
loader: async ({ params, actionResult, signal }) => {
|
|
194
|
+
// actionResult is { success: true, message: "Saved!" }
|
|
195
|
+
// after the action, or undefined on normal navigation
|
|
196
|
+
const data = await fetchData(params.id, signal);
|
|
197
|
+
return { ...data, actionResult };
|
|
198
|
+
},`}</CodeBlock>
|
|
199
|
+
<p>
|
|
200
|
+
This lets your UI display feedback from the action (e.g. success
|
|
201
|
+
messages or validation errors) alongside the refreshed data.
|
|
202
|
+
</p>
|
|
203
|
+
</section>
|
|
204
|
+
|
|
205
|
+
<section>
|
|
206
|
+
<h3>Summary</h3>
|
|
207
|
+
<ul>
|
|
208
|
+
<li>
|
|
209
|
+
<code>action</code> intercepts POST form submissions on the client
|
|
210
|
+
after hydration
|
|
211
|
+
</li>
|
|
212
|
+
<li>
|
|
213
|
+
Your server must handle the same POST endpoint for progressive
|
|
214
|
+
enhancement
|
|
215
|
+
</li>
|
|
216
|
+
<li>
|
|
217
|
+
The action’s return value flows to the loader as{" "}
|
|
218
|
+
<code>actionResult</code>
|
|
219
|
+
</li>
|
|
220
|
+
<li>
|
|
221
|
+
For SPAs without server-side form handling, prefer React 19’s{" "}
|
|
222
|
+
<code>{"<form action={fn}>"}</code> pattern
|
|
223
|
+
</li>
|
|
224
|
+
</ul>
|
|
225
|
+
</section>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -210,7 +210,7 @@ function App() {
|
|
|
210
210
|
Ephemeral <code>info</code>
|
|
211
211
|
</strong>{" "}
|
|
212
212
|
— Pass non-persisted context data during navigation via{" "}
|
|
213
|
-
<code>navigation.navigate(url, {"{ info }"})
|
|
213
|
+
<code>navigation.navigate(url, {"{ info }"})</code>
|
|
214
214
|
</li>
|
|
215
215
|
<li>
|
|
216
216
|
<strong>navigation.entries()</strong> — Access the full
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function LearnRouteDefinitionsPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="learn-content">
|
|
6
|
+
<h2>RSC with Route Features</h2>
|
|
7
|
+
|
|
8
|
+
<p className="page-intro">
|
|
9
|
+
When using React Server Components as route components, you may also
|
|
10
|
+
want route features like loaders, typed hooks (
|
|
11
|
+
<code>useRouteParams</code>, <code>useRouteData</code>), and navigation
|
|
12
|
+
state. The challenge is that route definitions referencing server
|
|
13
|
+
components cannot be imported from client modules. This guide shows how
|
|
14
|
+
to split a route definition into a <strong>shared part</strong>{" "}
|
|
15
|
+
(importable by client components for type safety) and a{" "}
|
|
16
|
+
<strong>server part</strong> (where the component is attached), enabling
|
|
17
|
+
full route features alongside RSC.
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<section>
|
|
21
|
+
<h3>The Problem</h3>
|
|
22
|
+
<p>
|
|
23
|
+
In an RSC architecture, server modules and client modules cannot
|
|
24
|
+
freely import from each other. This creates a dilemma for type-safe
|
|
25
|
+
routing:
|
|
26
|
+
</p>
|
|
27
|
+
<ul>
|
|
28
|
+
<li>
|
|
29
|
+
<strong>If routes live in a server module</strong> — they can
|
|
30
|
+
reference server components, but client components cannot import the
|
|
31
|
+
route objects for type-safe hooks like <code>useRouteParams</code>{" "}
|
|
32
|
+
or <code>useRouteData</code>.
|
|
33
|
+
</li>
|
|
34
|
+
<li>
|
|
35
|
+
<strong>If routes live in a shared module</strong> — client
|
|
36
|
+
components can import them, but server components cannot be
|
|
37
|
+
referenced (importing a server component makes a module
|
|
38
|
+
server-only).
|
|
39
|
+
</li>
|
|
40
|
+
</ul>
|
|
41
|
+
<p>
|
|
42
|
+
There is no single location where route objects can both reference
|
|
43
|
+
server components <em>and</em> be imported by client components.
|
|
44
|
+
</p>
|
|
45
|
+
</section>
|
|
46
|
+
|
|
47
|
+
<section>
|
|
48
|
+
<h3>The Key Insight</h3>
|
|
49
|
+
<p>
|
|
50
|
+
The only part of a route definition that is inherently server-specific
|
|
51
|
+
is the <strong>component</strong> (because it may be a server
|
|
52
|
+
component). Everything else — <code>id</code>, <code>path</code>
|
|
53
|
+
, <code>loader</code>, <code>action</code>, and navigation state
|
|
54
|
+
— is client-safe. Loaders and actions run in the browser during
|
|
55
|
+
navigation, so they can live in shared modules.
|
|
56
|
+
</p>
|
|
57
|
+
<p>
|
|
58
|
+
This means we can split a route definition at exactly one point: the
|
|
59
|
+
component reference. FUNSTACK Router supports this split through
|
|
60
|
+
partial route definitions and <code>bindRoute()</code>.
|
|
61
|
+
</p>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
<section>
|
|
65
|
+
<h3>Step 1: Define the Route (Shared Module)</h3>
|
|
66
|
+
<p>
|
|
67
|
+
Call <code>route()</code> <strong>without</strong> a{" "}
|
|
68
|
+
<code>component</code> property to create a partial route definition.
|
|
69
|
+
This object carries all type information and is safe to import from
|
|
70
|
+
client modules:
|
|
71
|
+
</p>
|
|
72
|
+
<CodeBlock language="typescript">{`// src/pages/user/loader.ts
|
|
73
|
+
"use client";
|
|
74
|
+
import type { User } from "../../types";
|
|
75
|
+
|
|
76
|
+
export async function loadUser({ params, signal }) {
|
|
77
|
+
const res = await fetch(\`/api/users/\${params.userId}\`, { signal });
|
|
78
|
+
return res.json() as Promise<User>;
|
|
79
|
+
}`}</CodeBlock>
|
|
80
|
+
<CodeBlock language="typescript">{`// src/pages/user/route.ts — shared module (no "use client" directive)
|
|
81
|
+
import { route } from "@funstack/router/server";
|
|
82
|
+
import { loadUser } from "./loader";
|
|
83
|
+
|
|
84
|
+
export const userRoute = route({
|
|
85
|
+
id: "user",
|
|
86
|
+
path: "/:userId",
|
|
87
|
+
loader: loadUser,
|
|
88
|
+
});
|
|
89
|
+
// Inferred types:
|
|
90
|
+
// Params = { userId: string } — from path
|
|
91
|
+
// Data = User — from loader return type`}</CodeBlock>
|
|
92
|
+
<p>
|
|
93
|
+
The <code>id</code> property is required for partial routes — it
|
|
94
|
+
is used at runtime to match the route context and at the type level to
|
|
95
|
+
carry type information for hooks.
|
|
96
|
+
</p>
|
|
97
|
+
</section>
|
|
98
|
+
|
|
99
|
+
<section>
|
|
100
|
+
<h3>Step 2: Bind the Component (Server Module)</h3>
|
|
101
|
+
<p>
|
|
102
|
+
Use <code>bindRoute()</code> from <code>@funstack/router/server</code>{" "}
|
|
103
|
+
to attach a component to the partial route. This produces a full route
|
|
104
|
+
definition for <code>{"<Router />"}</code>:
|
|
105
|
+
</p>
|
|
106
|
+
<CodeBlock language="tsx">{`// src/App.tsx — Server Component
|
|
107
|
+
import { bindRoute } from "@funstack/router/server";
|
|
108
|
+
import { Router } from "@funstack/router";
|
|
109
|
+
import { userRoute } from "./pages/user/route";
|
|
110
|
+
import { UserProfile } from "./pages/user/UserProfile";
|
|
111
|
+
|
|
112
|
+
const routes = [
|
|
113
|
+
bindRoute(userRoute, {
|
|
114
|
+
component: <UserProfile />,
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
export default function App() {
|
|
119
|
+
return <Router routes={routes} />;
|
|
120
|
+
}`}</CodeBlock>
|
|
121
|
+
<p>
|
|
122
|
+
Because <code>bindRoute()</code> lives in the server entry point, the
|
|
123
|
+
component can be a server component. The resulting route definition is
|
|
124
|
+
fully compatible with <code>{"<Router />"}</code> — it is the
|
|
125
|
+
same type as what <code>route()</code> with a component produces.
|
|
126
|
+
</p>
|
|
127
|
+
<p>
|
|
128
|
+
<code>bindRoute()</code> also accepts optional <code>children</code>,{" "}
|
|
129
|
+
<code>exact</code>, and <code>requireChildren</code> properties in the
|
|
130
|
+
second argument, just like the regular <code>route()</code> function.
|
|
131
|
+
</p>
|
|
132
|
+
</section>
|
|
133
|
+
|
|
134
|
+
<section>
|
|
135
|
+
<h3>Type-Safe Hooks in Client Components</h3>
|
|
136
|
+
<p>
|
|
137
|
+
The partial route object from Step 1 can be imported in client
|
|
138
|
+
components and passed to hooks for full type safety:
|
|
139
|
+
</p>
|
|
140
|
+
<CodeBlock language="tsx">{`// src/pages/user/UserActions.tsx
|
|
141
|
+
"use client";
|
|
142
|
+
import { useRouteParams, useRouteData } from "@funstack/router";
|
|
143
|
+
import { userRoute } from "./route";
|
|
144
|
+
|
|
145
|
+
export function UserActions() {
|
|
146
|
+
const { userId } = useRouteParams(userRoute);
|
|
147
|
+
// userId: string ✓
|
|
148
|
+
|
|
149
|
+
const user = useRouteData(userRoute);
|
|
150
|
+
// user: User ✓
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
<h2>{user.name}</h2>
|
|
155
|
+
<p>User ID: {userId}</p>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}`}</CodeBlock>
|
|
159
|
+
<p>
|
|
160
|
+
All typed hooks — <code>useRouteParams</code>,{" "}
|
|
161
|
+
<code>useRouteData</code>, and <code>useRouteState</code> —
|
|
162
|
+
accept both partial route definitions and full route definitions. The
|
|
163
|
+
type information flows naturally from path patterns, loader return
|
|
164
|
+
types, and <code>routeState</code>.
|
|
165
|
+
</p>
|
|
166
|
+
</section>
|
|
167
|
+
|
|
168
|
+
<section>
|
|
169
|
+
<h3>Navigation State</h3>
|
|
170
|
+
<p>
|
|
171
|
+
<code>routeState()</code> also supports partial route definitions.
|
|
172
|
+
When called without a <code>component</code>, it produces a partial
|
|
173
|
+
route carrying the state type:
|
|
174
|
+
</p>
|
|
175
|
+
<CodeBlock language="typescript">{`// src/pages/settings/route.ts — shared module
|
|
176
|
+
import { routeState } from "@funstack/router/server";
|
|
177
|
+
|
|
178
|
+
type SettingsState = { tab: string };
|
|
179
|
+
|
|
180
|
+
export const settingsRoute = routeState<SettingsState>()({
|
|
181
|
+
id: "settings",
|
|
182
|
+
path: "/settings",
|
|
183
|
+
});
|
|
184
|
+
// Params = {}, State = { tab: string }`}</CodeBlock>
|
|
185
|
+
<CodeBlock language="tsx">{`// src/pages/settings/SettingsPanel.tsx
|
|
186
|
+
"use client";
|
|
187
|
+
import { useRouteState } from "@funstack/router";
|
|
188
|
+
import { settingsRoute } from "./route";
|
|
189
|
+
|
|
190
|
+
export function SettingsPanel() {
|
|
191
|
+
const state = useRouteState(settingsRoute);
|
|
192
|
+
// state: { tab: string } | undefined ✓
|
|
193
|
+
// ...
|
|
194
|
+
}`}</CodeBlock>
|
|
195
|
+
</section>
|
|
196
|
+
|
|
197
|
+
<section>
|
|
198
|
+
<h3>Nested Routes</h3>
|
|
199
|
+
<p>
|
|
200
|
+
Partial routes use relative path segments, the same as regular routes.
|
|
201
|
+
Use <code>bindRoute()</code> with <code>children</code> to build
|
|
202
|
+
nested route trees:
|
|
203
|
+
</p>
|
|
204
|
+
<CodeBlock language="typescript">{`// src/pages/users/route.ts
|
|
205
|
+
import { route } from "@funstack/router/server";
|
|
206
|
+
export const usersRoute = route({ id: "users", path: "/users" });
|
|
207
|
+
|
|
208
|
+
// src/pages/users/profile/route.ts
|
|
209
|
+
import { route } from "@funstack/router/server";
|
|
210
|
+
import { fetchUser } from "./fetchUser"; // "use client" module
|
|
211
|
+
export const userProfileRoute = route({
|
|
212
|
+
id: "userProfile",
|
|
213
|
+
path: "/:userId", // relative to parent
|
|
214
|
+
loader: fetchUser,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// src/pages/users/settings/route.ts
|
|
218
|
+
import { route } from "@funstack/router/server";
|
|
219
|
+
export const userSettingsRoute = route({
|
|
220
|
+
id: "userSettings",
|
|
221
|
+
path: "/:userId/settings", // relative to parent
|
|
222
|
+
});`}</CodeBlock>
|
|
223
|
+
<CodeBlock language="tsx">{`// src/App.tsx
|
|
224
|
+
const routes = [
|
|
225
|
+
bindRoute(usersRoute, {
|
|
226
|
+
component: <Outlet />,
|
|
227
|
+
children: [
|
|
228
|
+
bindRoute(userProfileRoute, {
|
|
229
|
+
component: <UserProfile />,
|
|
230
|
+
}),
|
|
231
|
+
bindRoute(userSettingsRoute, {
|
|
232
|
+
component: <UserSettings />,
|
|
233
|
+
}),
|
|
234
|
+
],
|
|
235
|
+
}),
|
|
236
|
+
];`}</CodeBlock>
|
|
237
|
+
<p>
|
|
238
|
+
For layout routes that don't need typed hooks, <code>id</code> is
|
|
239
|
+
optional. A route without <code>id</code> can still be used with{" "}
|
|
240
|
+
<code>bindRoute()</code>:
|
|
241
|
+
</p>
|
|
242
|
+
<CodeBlock language="typescript">{`import { route, bindRoute } from "@funstack/router/server";
|
|
243
|
+
const layout = route({ path: "/dashboard" });
|
|
244
|
+
bindRoute(layout, { component: <Outlet />, children: [...] });`}</CodeBlock>
|
|
245
|
+
</section>
|
|
246
|
+
|
|
247
|
+
<section>
|
|
248
|
+
<h3>Recommended Project Structure</h3>
|
|
249
|
+
<p>
|
|
250
|
+
This pattern encourages <strong>collocating</strong> each route
|
|
251
|
+
definition with the page components that use it:
|
|
252
|
+
</p>
|
|
253
|
+
<CodeBlock language="bash">{`src/
|
|
254
|
+
pages/
|
|
255
|
+
user/
|
|
256
|
+
route.ts ← Step 1: id, path (shared module)
|
|
257
|
+
loader.ts ← "use client" — loader function
|
|
258
|
+
UserProfile.tsx ← Server component (the page)
|
|
259
|
+
UserActions.tsx ← "use client" — imports ./route for hooks
|
|
260
|
+
settings/
|
|
261
|
+
route.ts ← Step 1: id, path, routeState (shared module)
|
|
262
|
+
Settings.tsx ← Server component (the page)
|
|
263
|
+
SettingsPanel.tsx ← "use client" — imports ./route for hooks
|
|
264
|
+
App.tsx ← Step 2: bindRoute() assembles route tree`}</CodeBlock>
|
|
265
|
+
<p>This structure provides several benefits:</p>
|
|
266
|
+
<ul>
|
|
267
|
+
<li>
|
|
268
|
+
<strong>Locality</strong> — The route definition sits next to
|
|
269
|
+
the components that use it. Imports are short and obvious.
|
|
270
|
+
</li>
|
|
271
|
+
<li>
|
|
272
|
+
<strong>Encapsulation</strong> — Each page "owns" its route.
|
|
273
|
+
Adding a new page means adding a folder with a route and components,
|
|
274
|
+
then one <code>bindRoute()</code> call in <code>App.tsx</code>.
|
|
275
|
+
</li>
|
|
276
|
+
<li>
|
|
277
|
+
<strong>Local type safety</strong> — Path params and loader
|
|
278
|
+
data types are defined once in <code>route.ts</code> and consumed by
|
|
279
|
+
sibling client components. No separate type declarations needed.
|
|
280
|
+
</li>
|
|
281
|
+
</ul>
|
|
282
|
+
</section>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
@@ -269,6 +269,12 @@ export default function App() {
|
|
|
269
269
|
See also the <a href="/learn/ssr">Server-Side Rendering</a> guide
|
|
270
270
|
for how the router handles SSR and hydration
|
|
271
271
|
</li>
|
|
272
|
+
<li>
|
|
273
|
+
For type-safe hooks in client components, see the{" "}
|
|
274
|
+
<a href="/learn/rsc/route-features">RSC with Route Features</a>{" "}
|
|
275
|
+
guide which explains how to split route definitions across the
|
|
276
|
+
server/client boundary
|
|
277
|
+
</li>
|
|
272
278
|
</ul>
|
|
273
279
|
</section>
|
|
274
280
|
</div>
|
|
@@ -103,11 +103,9 @@ export function LearnSsgPage() {
|
|
|
103
103
|
</p>
|
|
104
104
|
<p>
|
|
105
105
|
If you only need loaders to run at build time (not on the client),
|
|
106
|
-
consider using
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
send the result as static HTML, without shipping loader code to the
|
|
110
|
-
client.
|
|
106
|
+
consider using <a href="/learn/rsc">React Server Components</a> with
|
|
107
|
+
SSG. RSC lets you fetch data on the server during the build and send
|
|
108
|
+
the result as static HTML, without shipping loader code to the client.
|
|
111
109
|
</p>
|
|
112
110
|
</section>
|
|
113
111
|
|
package/dist/docs/index.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
## Available Documentation
|
|
4
4
|
|
|
5
5
|
- [Examples](./ExamplesPage.tsx)
|
|
6
|
+
- [FAQ](./FaqPage.tsx)
|
|
6
7
|
- [Getting Started](./GettingStartedPage.tsx)
|
|
7
8
|
|
|
8
9
|
### API Reference
|
|
@@ -14,8 +15,10 @@
|
|
|
14
15
|
|
|
15
16
|
### Learn
|
|
16
17
|
|
|
18
|
+
- [Form Actions](./LearnActionsPage.tsx) - FUNSTACK Router can intercept submissions and run an action function on the client before navigation occurs. This guide explains how actions work, when to use them, and important considerations for progressive enhancement.
|
|
17
19
|
- [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.
|
|
18
20
|
- [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.
|
|
21
|
+
- [RSC with Route Features](./LearnRouteDefinitionsPage.tsx) - When using React Server Components as route components, you may also want route features like loaders, typed hooks ( useRouteParams, useRouteData), and navigation state. The challenge is that route definitions referencing server components cannot be imported from client modules. This guide shows how to split a route definition into a shared part (importable by client components for type safety) and a server part (where the component is attached), enabling full route features alongside RSC.
|
|
19
22
|
- [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.
|
|
20
23
|
- [Static Site Generation](./LearnSsgPage.tsx) - When your server or static site generator knows the URL being rendered, you can use the ssr prop to match path-based routes during SSR. This produces richer server-rendered HTML — users see page content immediately instead of just the app shell.
|
|
21
24
|
- [How SSR Works](./LearnSsrBasicPage.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.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as routeState, a as ExtractRouteParams, c as OpaqueRouteDefinition, d as RouteComponentProps, f as RouteComponentPropsOf, g as route, h as TypefulOpaqueRouteDefinition, i as ExtractRouteId, l as PartialRouteDefinition, m as RouteDefinition, n as ActionArgs, o as ExtractRouteState, p as RouteComponentPropsWithData, r as ExtractRouteData, s as LoaderArgs, t as bindRoute, u as PathParams } from "./bindRoute-BtT4qPKI.mjs";
|
|
2
2
|
import { ComponentType, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/types.d.ts
|
|
@@ -167,12 +167,6 @@ declare function Router({
|
|
|
167
167
|
*/
|
|
168
168
|
declare function Outlet(): ReactNode;
|
|
169
169
|
//#endregion
|
|
170
|
-
//#region src/hooks/useNavigate.d.ts
|
|
171
|
-
/**
|
|
172
|
-
* Returns a function for programmatic navigation.
|
|
173
|
-
*/
|
|
174
|
-
declare function useNavigate(): (to: string, options?: NavigateOptions) => void;
|
|
175
|
-
//#endregion
|
|
176
170
|
//#region src/hooks/useLocation.d.ts
|
|
177
171
|
/**
|
|
178
172
|
* Returns the current location object.
|
|
@@ -244,7 +238,7 @@ declare function useBlocker(options: UseBlockerOptions): void;
|
|
|
244
238
|
* }
|
|
245
239
|
* ```
|
|
246
240
|
*/
|
|
247
|
-
declare function useRouteParams<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteParams<T>;
|
|
241
|
+
declare function useRouteParams<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown> | PartialRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteParams<T>;
|
|
248
242
|
//#endregion
|
|
249
243
|
//#region src/hooks/useRouteState.d.ts
|
|
250
244
|
/**
|
|
@@ -268,7 +262,7 @@ declare function useRouteParams<T extends TypefulOpaqueRouteDefinition<string, R
|
|
|
268
262
|
* }
|
|
269
263
|
* ```
|
|
270
264
|
*/
|
|
271
|
-
declare function useRouteState<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteState<T> | undefined;
|
|
265
|
+
declare function useRouteState<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown> | PartialRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteState<T> | undefined;
|
|
272
266
|
//#endregion
|
|
273
267
|
//#region src/hooks/useRouteData.d.ts
|
|
274
268
|
/**
|
|
@@ -295,7 +289,7 @@ declare function useRouteState<T extends TypefulOpaqueRouteDefinition<string, Re
|
|
|
295
289
|
* }
|
|
296
290
|
* ```
|
|
297
291
|
*/
|
|
298
|
-
declare function useRouteData<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteData<T>;
|
|
292
|
+
declare function useRouteData<T extends TypefulOpaqueRouteDefinition<string, Record<string, string>, unknown, unknown> | PartialRouteDefinition<string, Record<string, string>, unknown, unknown>>(route: T): ExtractRouteData<T>;
|
|
299
293
|
//#endregion
|
|
300
294
|
//#region src/hooks/useIsPending.d.ts
|
|
301
295
|
/**
|
|
@@ -303,5 +297,16 @@ declare function useRouteData<T extends TypefulOpaqueRouteDefinition<string, Rec
|
|
|
303
297
|
*/
|
|
304
298
|
declare function useIsPending(): boolean;
|
|
305
299
|
//#endregion
|
|
306
|
-
|
|
300
|
+
//#region src/bypassInterception.d.ts
|
|
301
|
+
/**
|
|
302
|
+
* Perform a full page reload, bypassing the router's interception.
|
|
303
|
+
*/
|
|
304
|
+
declare function hardReload(): void;
|
|
305
|
+
/**
|
|
306
|
+
* Navigate to the given URL with a full page navigation,
|
|
307
|
+
* bypassing the router's interception.
|
|
308
|
+
*/
|
|
309
|
+
declare function hardNavigate(url: string): void;
|
|
310
|
+
//#endregion
|
|
311
|
+
export { type ActionArgs, type ExtractRouteData, type ExtractRouteId, type ExtractRouteParams, type ExtractRouteState, type FallbackMode, type LoaderArgs, type Location, type MatchedRoute, type NavigateOptions, type OnNavigateCallback, type OnNavigateInfo, type OpaqueRouteDefinition, Outlet, type PartialRouteDefinition, type PathParams, type RouteComponentProps, type RouteComponentPropsOf, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type SSRConfig, type TypefulOpaqueRouteDefinition, type UseBlockerOptions, bindRoute, hardNavigate, hardReload, route, routeState, useBlocker, useIsPending, useLocation, useRouteData, useRouteParams, useRouteState, useSearchParams };
|
|
307
312
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router/index.tsx","../src/Outlet.tsx","../src/hooks/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/Router/index.tsx","../src/Outlet.tsx","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/hooks/useRouteParams.ts","../src/hooks/useRouteState.ts","../src/hooks/useRouteData.ts","../src/hooks/useIsPending.ts","../src/bypassInterception.ts"],"mappings":";;;;cAGM,6BAAA;;AAFoE;;;;;;;KAgB9D,uBAAA;EAAA,CACT,6BAAA,UAyB0B;EAvB3B,IAAA,WA4Be;EA1Bf,QAAA,GAAW,uBAAA;EAgCc;;;;;;EAzBzB,KAAA;EAPA;;;;;EAaA,eAAA,YAM2B;EAA3B,MAAA,IAAU,IAAA,EAAM,UAAA,CAAW,MAAA,+BAE3B;EAAA,MAAA,IAAU,IAAA,EAAM,UAAA,CAAW,MAAA,wCAAA;EAE3B,SAAA,GACI,aAAA;IACE,IAAA;IACA,MAAA,GAAS,MAAA;IACT,KAAA;IACA,QAAA,IACE,KAAA,cAAmB,IAAA,2BAChB,OAAA;IACL,YAAA,IAAgB,KAAA,cAAmB,IAAA;IACnC,UAAA,SAAmB,OAAA;IACnB,cAAA;IACA,IAAA;EAAA,KAEF,SAAA;AAAA;;;AAmBN;KAAY,YAAA;sCAEV,KAAA,EAAO,uBAAA,EAAP;EAEA,MAAA,EAAQ,MAAA,kBAAR;EAEA,QAAA;AAAA;;;;KAcU,cAAA;EAMQ,4DAJlB,OAAA,WAAkB,YAAA,WAUR;EARV,YAAA;EAEA,QAAA,EAAU,QAAA;AAAA;;;;KAMA,eAAA;EAYA,uDAVV,OAAA;EAEA,KAAA,YASA;EAPA,IAAA;AAAA;;;AAmBF;KAbY,QAAA;EACV,QAAA;EACA,MAAA;EACA,IAAA;AAAA;;;;;AAqBF;;;KAXY,kBAAA,IACV,KAAA,EAAO,aAAA,EACP,IAAA,EAAM,cAAA;;;;;AChGR;;KDyGY,YAAA;;;;AAhJ8D;;KCuC9D,SAAA;EDrCkC;;AAc9C;;;;EC8BE,IAAA;EDN2B;;;;;;;;;;;ECkB3B,UAAA;AAAA;AAAA,KAGU,WAAA;EACV,MAAA,EAAQ,eAAA;EDlCR;;;;;;;EC0CA,UAAA,GAAa,kBAAA;ED5Bc;;;;;;ECmC3B,QAAA,GAAW,YAAA;ED7BL;;;;;;;;;;;;;;;AA4BR;;;;;ECsBE,GAAA,GAAM,SAAA;AAAA;AAAA,iBAGQ,MAAA,CAAA;EACd,MAAA,EAAQ,WAAA;EACR,UAAA;EACA,QAAA;EACA;AAAA,GACC,WAAA,GAAc,SAAA;;;;;;AD1GyD;iBEM1D,MAAA,CAAA,GAAU,SAAA;;;;;;iBCAV,WAAA,CAAA,GAAe,QAAA;;;KCJ1B,eAAA,IACH,MAAA,EACI,eAAA,GACA,MAAA,qBACE,IAAA,EAAM,eAAA,KAAoB,eAAA,GAAkB,MAAA;;;;iBAMpC,eAAA,CAAA,IAAoB,eAAA,EAAiB,eAAA;;;KCVzC,iBAAA;;;;ALF8D;EKOxE,WAAA;AAAA;;;ALSF;;;;;;;;;;;;;;;;;;;;;;;;;iBKqBgB,UAAA,CAAW,OAAA,EAAS,iBAAA;;;;;;ALrCsC;;;;;AAgB1E;;;;;;;;;;;;iBMUgB,cAAA,WAEV,4BAAA,SAEE,MAAA,sCAIF,sBAAA,SAA+B,MAAA,oCAAA,CACnC,KAAA,EAAO,CAAA,GAAI,kBAAA,CAAmB,CAAA;;;;;;ANnC0C;;;;;AAgB1E;;;;;;;;;;;;;iBOWgB,aAAA,WAEV,4BAAA,SAEE,MAAA,sCAIF,sBAAA,SAA+B,MAAA,oCAAA,CACnC,KAAA,EAAO,CAAA,GAAI,iBAAA,CAAkB,CAAA;;;;;;APpC2C;;;;;AAgB1E;;;;;;;;;;;;;;;;iBQcgB,YAAA,WAEV,4BAAA,SAEE,MAAA,sCAIF,sBAAA,SAA+B,MAAA,oCAAA,CACnC,KAAA,EAAO,CAAA,GAAI,gBAAA,CAAiB,CAAA;;;;;;iBClCd,YAAA,CAAA;;;ATL0D;;;AAAA,iBUW1D,UAAA,CAAA;;AVKhB;;;iBUGgB,YAAA,CAAa,GAAA"}
|