@funstack/router 0.0.7-alpha.0 → 0.0.8

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