@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,186 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function GettingStartedPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="page docs-page">
|
|
6
|
+
<h1>Getting Started</h1>
|
|
7
|
+
|
|
8
|
+
<section>
|
|
9
|
+
<h2>Installation</h2>
|
|
10
|
+
<p>Install the package using your preferred package manager:</p>
|
|
11
|
+
<CodeBlock language="bash">{`npm install @funstack/router
|
|
12
|
+
# or
|
|
13
|
+
pnpm add @funstack/router
|
|
14
|
+
# or
|
|
15
|
+
yarn add @funstack/router`}</CodeBlock>
|
|
16
|
+
</section>
|
|
17
|
+
|
|
18
|
+
<section>
|
|
19
|
+
<h2>Browser Support</h2>
|
|
20
|
+
<p>
|
|
21
|
+
FUNSTACK Router uses the{" "}
|
|
22
|
+
<a
|
|
23
|
+
href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API"
|
|
24
|
+
target="_blank"
|
|
25
|
+
rel="noopener noreferrer"
|
|
26
|
+
>
|
|
27
|
+
Navigation API
|
|
28
|
+
</a>{" "}
|
|
29
|
+
which is supported in Chrome 102+, Edge 102+, Firefox 147+, Safari
|
|
30
|
+
26.2+, and Opera 88+.
|
|
31
|
+
</p>
|
|
32
|
+
</section>
|
|
33
|
+
|
|
34
|
+
<section>
|
|
35
|
+
<h2>Basic Setup</h2>
|
|
36
|
+
<p>
|
|
37
|
+
Create your routes using the <code>route</code> helper function and
|
|
38
|
+
render them with the <code>Router</code> component:
|
|
39
|
+
</p>
|
|
40
|
+
<CodeBlock language="tsx">{`import { Router, route, Outlet } from "@funstack/router";
|
|
41
|
+
|
|
42
|
+
// Define your page components
|
|
43
|
+
function Home() {
|
|
44
|
+
return <h1>Welcome Home</h1>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function About() {
|
|
48
|
+
return <h1>About Us</h1>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function Layout() {
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<nav>
|
|
55
|
+
<a href="/">Home</a>
|
|
56
|
+
<a href="/about">About</a>
|
|
57
|
+
</nav>
|
|
58
|
+
<Outlet />
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Define your routes
|
|
64
|
+
const routes = [
|
|
65
|
+
route({
|
|
66
|
+
path: "/",
|
|
67
|
+
component: Layout,
|
|
68
|
+
children: [
|
|
69
|
+
route({ path: "/", component: Home }),
|
|
70
|
+
route({ path: "/about", component: About }),
|
|
71
|
+
],
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// Render the router
|
|
76
|
+
function App() {
|
|
77
|
+
return <Router routes={routes} />;
|
|
78
|
+
}`}</CodeBlock>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<section>
|
|
82
|
+
<h2>Route Parameters</h2>
|
|
83
|
+
<p>
|
|
84
|
+
Define dynamic segments in your paths using the <code>:param</code>{" "}
|
|
85
|
+
syntax. Route components receive parameters via the{" "}
|
|
86
|
+
<code>params</code> prop, which is fully typed based on the path
|
|
87
|
+
pattern:
|
|
88
|
+
</p>
|
|
89
|
+
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
90
|
+
|
|
91
|
+
function UserProfile({ params }: { params: { userId: string } }) {
|
|
92
|
+
return <h1>User: {params.userId}</h1>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const routes = [
|
|
96
|
+
route({
|
|
97
|
+
path: "/users/:userId",
|
|
98
|
+
component: UserProfile,
|
|
99
|
+
}),
|
|
100
|
+
];`}</CodeBlock>
|
|
101
|
+
<p>
|
|
102
|
+
Alternatively, you can use the <code>useParams</code> hook to access
|
|
103
|
+
parameters:
|
|
104
|
+
</p>
|
|
105
|
+
<CodeBlock language="tsx">{`import { useParams } from "@funstack/router";
|
|
106
|
+
|
|
107
|
+
function UserProfile() {
|
|
108
|
+
const params = useParams<{ userId: string }>();
|
|
109
|
+
return <h1>User: {params.userId}</h1>;
|
|
110
|
+
}`}</CodeBlock>
|
|
111
|
+
</section>
|
|
112
|
+
|
|
113
|
+
<section>
|
|
114
|
+
<h2>Programmatic Navigation</h2>
|
|
115
|
+
<p>
|
|
116
|
+
Use the <code>useNavigate</code> hook for programmatic navigation:
|
|
117
|
+
</p>
|
|
118
|
+
<CodeBlock language="tsx">{`import { useNavigate } from "@funstack/router";
|
|
119
|
+
|
|
120
|
+
function MyComponent() {
|
|
121
|
+
const navigate = useNavigate();
|
|
122
|
+
|
|
123
|
+
const handleClick = () => {
|
|
124
|
+
navigate("/about");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return <button onClick={handleClick}>Go to About</button>;
|
|
128
|
+
}`}</CodeBlock>
|
|
129
|
+
</section>
|
|
130
|
+
|
|
131
|
+
<section>
|
|
132
|
+
<h2>Data Loading</h2>
|
|
133
|
+
<p>
|
|
134
|
+
Use the <code>loader</code> option to fetch data before rendering a
|
|
135
|
+
route. The component receives both <code>data</code> (from the loader)
|
|
136
|
+
and <code>params</code> (from the URL) as props:
|
|
137
|
+
</p>
|
|
138
|
+
<CodeBlock language="tsx">{`import { route } from "@funstack/router";
|
|
139
|
+
|
|
140
|
+
interface User {
|
|
141
|
+
id: string;
|
|
142
|
+
name: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function UserProfilePage({
|
|
146
|
+
data,
|
|
147
|
+
params,
|
|
148
|
+
}: {
|
|
149
|
+
data: Promise<User>;
|
|
150
|
+
params: { userId: string };
|
|
151
|
+
}) {
|
|
152
|
+
const user = use(data);
|
|
153
|
+
return (
|
|
154
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
155
|
+
<UserProfile user={user} params={params} />
|
|
156
|
+
</Suspense>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function UserProfile({
|
|
161
|
+
user,
|
|
162
|
+
params,
|
|
163
|
+
}: {
|
|
164
|
+
user: User;
|
|
165
|
+
params: { userId: string };
|
|
166
|
+
}) {
|
|
167
|
+
return (
|
|
168
|
+
<div>
|
|
169
|
+
<h1>{user.name}</h1>
|
|
170
|
+
<p>User ID: {params.userId}</p>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const userRoute = route({
|
|
176
|
+
path: "/users/:userId",
|
|
177
|
+
component: UserProfilePage,
|
|
178
|
+
loader: async ({ params }): Promise<User> => {
|
|
179
|
+
const response = await fetch(\`/api/users/\${params.userId}\`);
|
|
180
|
+
return response.json();
|
|
181
|
+
},
|
|
182
|
+
});`}</CodeBlock>
|
|
183
|
+
</section>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { CodeBlock } from "../components/CodeBlock.js";
|
|
2
|
+
|
|
3
|
+
export function LearnNavigationApiPage() {
|
|
4
|
+
return (
|
|
5
|
+
<div className="learn-content">
|
|
6
|
+
<h2>Navigation API</h2>
|
|
7
|
+
|
|
8
|
+
<p className="page-intro">
|
|
9
|
+
FUNSTACK Router is built on the{" "}
|
|
10
|
+
<a
|
|
11
|
+
href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API"
|
|
12
|
+
target="_blank"
|
|
13
|
+
rel="noopener noreferrer"
|
|
14
|
+
>
|
|
15
|
+
Navigation API
|
|
16
|
+
</a>
|
|
17
|
+
, a modern browser API that provides a unified way to handle navigation.
|
|
18
|
+
This guide explains the key differences from the older History API and
|
|
19
|
+
the benefits this brings to your application.
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<section>
|
|
23
|
+
<h3>History API Limitations</h3>
|
|
24
|
+
<p>
|
|
25
|
+
The History API has been the foundation of single-page application
|
|
26
|
+
routing for years, but it comes with significant limitations that make
|
|
27
|
+
building routers more complex than necessary.
|
|
28
|
+
</p>
|
|
29
|
+
<p>
|
|
30
|
+
<strong>Reactive, not proactive:</strong> The <code>popstate</code>{" "}
|
|
31
|
+
event fires <em>after</em> navigation has already occurred. This makes
|
|
32
|
+
it difficult to intercept navigation, perform async operations like
|
|
33
|
+
data fetching, or cancel navigation based on conditions.
|
|
34
|
+
</p>
|
|
35
|
+
<CodeBlock language="typescript">{`// History API: popstate fires AFTER navigation
|
|
36
|
+
window.addEventListener("popstate", (event) => {
|
|
37
|
+
// Too late! The URL has already changed.
|
|
38
|
+
// We can only react to what happened.
|
|
39
|
+
console.log("Navigated to:", location.pathname);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// For programmatic navigation, we must manually call pushState
|
|
43
|
+
// AND then update the UI ourselves
|
|
44
|
+
history.pushState(null, "", "/new-page");
|
|
45
|
+
updateUI(); // Manually trigger UI update`}</CodeBlock>
|
|
46
|
+
<p>
|
|
47
|
+
<strong>Fragmented event model:</strong> Different navigation types
|
|
48
|
+
require different handling. Browser back/forward triggers{" "}
|
|
49
|
+
<code>popstate</code>, but <code>pushState</code> and{" "}
|
|
50
|
+
<code>replaceState</code> don't trigger any events. This leads to
|
|
51
|
+
scattered navigation logic.
|
|
52
|
+
</p>
|
|
53
|
+
<p>
|
|
54
|
+
<strong>Custom Link components required:</strong> Since clicking an{" "}
|
|
55
|
+
<code>{"<a>"}</code> tag causes a full page reload, routers must
|
|
56
|
+
provide custom <code>{"<Link>"}</code> components that intercept
|
|
57
|
+
clicks and call <code>pushState</code> instead.
|
|
58
|
+
</p>
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
<section>
|
|
62
|
+
<h3>The Navigation API Paradigm</h3>
|
|
63
|
+
<p>
|
|
64
|
+
The Navigation API fundamentally changes how navigation works by
|
|
65
|
+
providing a single <code>navigate</code> event that fires{" "}
|
|
66
|
+
<em>before</em> navigation commits. This allows routers to intercept
|
|
67
|
+
and handle navigation declaratively.
|
|
68
|
+
</p>
|
|
69
|
+
<CodeBlock language="typescript">{`// Navigation API: navigate event fires BEFORE navigation
|
|
70
|
+
navigation.addEventListener("navigate", (event) => {
|
|
71
|
+
// Navigation hasn't happened yet - we can intercept it!
|
|
72
|
+
const url = new URL(event.destination.url);
|
|
73
|
+
|
|
74
|
+
// Check the navigation type
|
|
75
|
+
console.log(event.navigationType); // "push", "replace", "reload", or "traverse"
|
|
76
|
+
|
|
77
|
+
// Intercept and handle with async operations
|
|
78
|
+
event.intercept({
|
|
79
|
+
async handler() {
|
|
80
|
+
// Fetch data, render components, etc.
|
|
81
|
+
const data = await fetchPageData(url.pathname);
|
|
82
|
+
renderPage(data);
|
|
83
|
+
// Navigation completes when handler resolves
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});`}</CodeBlock>
|
|
87
|
+
<p>Key benefits of this approach:</p>
|
|
88
|
+
<ul>
|
|
89
|
+
<li>
|
|
90
|
+
<strong>Single unified event</strong> — All navigation types
|
|
91
|
+
(link clicks, back/forward, programmatic) trigger the same{" "}
|
|
92
|
+
<code>navigate</code> event
|
|
93
|
+
</li>
|
|
94
|
+
<li>
|
|
95
|
+
<strong>Interception with async support</strong> — The{" "}
|
|
96
|
+
<code>intercept()</code> method lets you run async handlers before
|
|
97
|
+
navigation completes
|
|
98
|
+
</li>
|
|
99
|
+
<li>
|
|
100
|
+
<strong>Built-in navigation type detection</strong> —{" "}
|
|
101
|
+
<code>event.navigationType</code> tells you exactly what kind of
|
|
102
|
+
navigation occurred
|
|
103
|
+
</li>
|
|
104
|
+
</ul>
|
|
105
|
+
</section>
|
|
106
|
+
|
|
107
|
+
<section>
|
|
108
|
+
<h3>
|
|
109
|
+
Native <code>{"<a>"}</code> Elements for SPA Links
|
|
110
|
+
</h3>
|
|
111
|
+
<p>
|
|
112
|
+
One of the most practical benefits of the Navigation API is that
|
|
113
|
+
standard <code>{"<a>"}</code> elements work for SPA navigation without
|
|
114
|
+
any special handling. The router intercepts navigation events from
|
|
115
|
+
native links automatically.
|
|
116
|
+
</p>
|
|
117
|
+
<CodeBlock language="tsx">{`// With FUNSTACK Router, native <a> elements just work!
|
|
118
|
+
function Navigation() {
|
|
119
|
+
return (
|
|
120
|
+
<nav>
|
|
121
|
+
{/* These are standard HTML anchor tags */}
|
|
122
|
+
<a href="/home">Home</a>
|
|
123
|
+
<a href="/about">About</a>
|
|
124
|
+
<a href="/users/123">User Profile</a>
|
|
125
|
+
</nav>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// No special <Link> component needed for basic navigation
|
|
130
|
+
// The router intercepts the navigate event and handles it as SPA navigation`}</CodeBlock>
|
|
131
|
+
<p>
|
|
132
|
+
This means you can use standard HTML in your components, and
|
|
133
|
+
navigation "just works". The router intercepts same-origin navigation
|
|
134
|
+
events and handles them without a full page reload.
|
|
135
|
+
</p>
|
|
136
|
+
<p>This approach has several advantages over custom Link components:</p>
|
|
137
|
+
<ul>
|
|
138
|
+
<li>
|
|
139
|
+
<strong>Standard HTML</strong> — No need to import and use
|
|
140
|
+
special components for every link
|
|
141
|
+
</li>
|
|
142
|
+
<li>
|
|
143
|
+
<strong>Progressive enhancement</strong> — Links work even if
|
|
144
|
+
JavaScript fails to load
|
|
145
|
+
</li>
|
|
146
|
+
<li>
|
|
147
|
+
<strong>Familiar patterns</strong> — Developers can use the{" "}
|
|
148
|
+
<code>{"<a>"}</code> tag they already know
|
|
149
|
+
</li>
|
|
150
|
+
</ul>
|
|
151
|
+
</section>
|
|
152
|
+
|
|
153
|
+
<section>
|
|
154
|
+
<h3>Accessing Navigation API Events</h3>
|
|
155
|
+
<p>
|
|
156
|
+
While FUNSTACK Router handles navigation for you, you can interact
|
|
157
|
+
directly with the Navigation API when needed. This is useful for
|
|
158
|
+
features like scroll-to-top behavior or analytics tracking.
|
|
159
|
+
</p>
|
|
160
|
+
<CodeBlock language="tsx">{`import { useEffect } from "react";
|
|
161
|
+
|
|
162
|
+
function App() {
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const navigation = window.navigation;
|
|
165
|
+
if (!navigation) return;
|
|
166
|
+
|
|
167
|
+
const controller = new AbortController();
|
|
168
|
+
|
|
169
|
+
// Listen for successful navigation completion
|
|
170
|
+
navigation.addEventListener(
|
|
171
|
+
"navigatesuccess",
|
|
172
|
+
() => {
|
|
173
|
+
// Track page view for analytics
|
|
174
|
+
analytics.trackPageView(location.pathname);
|
|
175
|
+
},
|
|
176
|
+
{ signal: controller.signal }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return () => controller.abort();
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
return <Router routes={routes} />;
|
|
183
|
+
}`}</CodeBlock>
|
|
184
|
+
<p>
|
|
185
|
+
The <code>navigatesuccess</code> event fires after navigation
|
|
186
|
+
completes successfully, making it ideal for post-navigation actions.
|
|
187
|
+
You can also use <code>navigation.transition</code> to track in-flight
|
|
188
|
+
navigations or implement loading indicators.
|
|
189
|
+
</p>
|
|
190
|
+
<p>
|
|
191
|
+
<strong>Note:</strong> be careful when adding event listeners for{" "}
|
|
192
|
+
<code>navigate</code> events since it may interfere with the router's
|
|
193
|
+
own handling. Consider using <code>onNavigate</code> prop on the{" "}
|
|
194
|
+
<code>{"<Router>"}</code> component for most use cases.
|
|
195
|
+
</p>
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
<section>
|
|
199
|
+
<h3>Other Navigation API Features</h3>
|
|
200
|
+
<p>
|
|
201
|
+
The Navigation API provides several advanced features that you can
|
|
202
|
+
leverage directly when needed:
|
|
203
|
+
</p>
|
|
204
|
+
<ul>
|
|
205
|
+
<li>
|
|
206
|
+
<strong>NavigationHistoryEntry</strong> — Each history entry
|
|
207
|
+
has a unique <code>id</code> and <code>key</code>, plus a{" "}
|
|
208
|
+
<code>dispose</code> event that fires when the entry is removed from
|
|
209
|
+
history
|
|
210
|
+
</li>
|
|
211
|
+
<li>
|
|
212
|
+
<strong>
|
|
213
|
+
Ephemeral <code>info</code>
|
|
214
|
+
</strong>{" "}
|
|
215
|
+
— Pass non-persisted context data during navigation via{" "}
|
|
216
|
+
<code>navigation.navigate(url, {"{ info }"}))</code>
|
|
217
|
+
</li>
|
|
218
|
+
<li>
|
|
219
|
+
<strong>navigation.entries()</strong> — Access the full
|
|
220
|
+
navigation history stack programmatically
|
|
221
|
+
</li>
|
|
222
|
+
</ul>
|
|
223
|
+
<p>
|
|
224
|
+
For more details, see the{" "}
|
|
225
|
+
<a
|
|
226
|
+
href="https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API"
|
|
227
|
+
target="_blank"
|
|
228
|
+
rel="noopener noreferrer"
|
|
229
|
+
>
|
|
230
|
+
MDN Navigation API documentation
|
|
231
|
+
</a>
|
|
232
|
+
.
|
|
233
|
+
</p>
|
|
234
|
+
</section>
|
|
235
|
+
|
|
236
|
+
<section>
|
|
237
|
+
<h3>Key Takeaways</h3>
|
|
238
|
+
<ul>
|
|
239
|
+
<li>
|
|
240
|
+
<strong>
|
|
241
|
+
Native <code>{"<a>"}</code> elements work
|
|
242
|
+
</strong>{" "}
|
|
243
|
+
— No special Link component required for SPA navigation; the
|
|
244
|
+
router intercepts standard anchor tags automatically
|
|
245
|
+
</li>
|
|
246
|
+
<li>
|
|
247
|
+
<strong>Direct API access when needed</strong> — Use events
|
|
248
|
+
like <code>navigatesuccess</code> for scroll restoration, analytics,
|
|
249
|
+
or other post-navigation actions
|
|
250
|
+
</li>
|
|
251
|
+
</ul>
|
|
252
|
+
</section>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|