INDIE UNPKG

7.82 kBMarkdownView Raw
1---
2title: Application-Controlled Route Discovery
3unstable: true
4---
5
6# Application-Controlled Route Discovery
7
8[MODES: framework]
9
10<docs-warning>This API is unstable and may change without a major release. It supports conventional Framework Mode; RSC modes are not supported.</docs-warning>
11
12Enable the future flag in `react-router.config.ts`:
13
14```ts
15import type { Config } from "@react-router/dev/config";
16
17export default {
18 future: { unstable_customRouteDiscovery: true },
19} satisfies Config;
20```
21
22Without this flag, React Router keeps its existing discovery implementation and the hook is unavailable. The flag supports conventional Framework Mode only.
23
24Keep `routeDiscovery.mode: "lazy"` (the default for SSR) and use `unstable_useRouteDiscovery` to choose when to discover destinations or load the complete route manifest.
25
26```tsx
27let { state, discoverRoutes, loadAllRoutes } =
28 unstable_useRouteDiscovery({
29 onManifestMismatch: async (event) => {
30 // Handle mismatch, potentially by loading the entire manifest
31 await loadAllRoutes();
32 },
33 });
34
35// state: "partial" | "loading" | "complete"
36
37// Eagerly discover specific paths
38let result = await discoverRoutes(paths);
39```
40
41## Imperative Route Discovery
42
43You can control route discovery through `discoverRoutes()` without having to render links/forms on the page - this allows you to eagerly discover routes that are not directly linked from the current UI. Note that discovery only loads route metadata - it does not navigate, run loaders/actions, or prefetch route modules/stylesheets. Paths are batched within the manifest request URL limit and previously discovered paths are skipped.
44
45`discoverRoutes(paths, { signal })` accepts application-root paths beginning with `/`. React Router applies the configured `basename` and ignores query strings and hashes. Relative paths and external URLs are not supported.
46
47The result is `{ type: "success" }`, `{ type: "version-mismatch" }`, or `{ type: "aborted" }`. Success means the discovery operation completed successfully; it does not guarantee that a request was sent or that each pathname matches a route. Cached paths need no request, and a concurrent full-manifest load can supersede an incremental request failure. Unrecovered request failures and unrelated handler errors reject. Imperative mismatches do not automatically trigger a reload.
48
49For example, the cart's checkout link automatically discovers the shipping route. Use the hook to discover later checkout steps when the cart mounts, even though those steps aren't linked from the cart:
50
51```tsx
52import { useEffect } from "react";
53import {
54 Link,
55 unstable_useRouteDiscovery,
56} from "react-router";
57
58function Cart() {
59 let { discoverRoutes } = unstable_useRouteDiscovery();
60
61 useEffect(() => {
62 let controller = new AbortController();
63
64 discoverRoutes(
65 ["/checkout/billing", "/checkout/review"],
66 { signal: controller.signal },
67 ).catch(console.error);
68
69 return () => controller.abort();
70 }, [discoverRoutes]);
71
72 return (
73 <>
74 {/* Render cart items and totals here. */}
75 <Link to="/checkout/shipping">
76 Proceed to checkout
77 </Link>
78 </>
79 );
80}
81```
82
83## Completing discovery
84
85`loadAllRoutes()` loads the running client's versioned manifest asset to complete its route tree, even if that client is stale. This merges all missing routes into the running router and thus prevents further incremental discovery. The exposed `version` remains the version of the running client. With `routeDiscovery.mode: "initial"`, SPA Mode, or development's full manifest, the state is already `complete` and the operations are successful no-ops.
86
87The shared `state` moves from `partial` to `loading` to `complete`. Concurrent calls share a load, and calls after completion resolve immediately. Failures reject and return the state to `partial`; applications can catch the error and choose whether to reload.
88
89Retain the client's versioned manifest **and its referenced assets** for as long as that client is supported. Server routing and loader/action contracts must also remain compatible. Loading build A's manifest does not make an incompatible build B backend safe for build A's client.
90
91## Handling manifest mismatches
92
93You can provide custom handling for manifest version mismatches to avoid hard reloads, giving you a chance to complete the route tree, or cancel the navigation and provide a prompt for the user to reload (see below). A mismatch indicates different versions, not which version is newer.
94
95```tsx
96unstable_useRouteDiscovery({
97 onManifestMismatch: async (event) => {
98 await event.loadAllRoutes();
99 },
100});
101```
102
103After mismatch handlers settle, surviving operations continue only if the tree is complete. Otherwise navigations and fetchers are canceled and their pending state settles. Canceling a navigation also cancels any pending revalidation - both return to idle and the current page and data remain in place without another loader pass. Similarly, interrupted fetcher loads settle with their existing data. If an action already changed server data, the displayed data may remain stale until a later navigation or explicit revalidation.
104
105Router-managed Back/Forward cancellations restore the prior history position. Additional browser Back/Forward presses during restoration can leave the URL and rendered route out of sync or navigate to another entry; preserving the current screen is not guaranteed in that case.
106
107Full-manifest load failures remain catchable and do not render route error boundaries, whether the handler calls `event.loadAllRoutes()` or the function returned by the hook. Uncaught full-load failures are reported through `<HydratedRouter onError>` or `console.error`, and the incomplete operation is canceled. Unrelated handler errors follow normal discovery error handling.
108
109## Temporary overrides
110
111Custom mismatch handlers override built-in recovery. Calling the hook without a handler does not override anything. Removing the last custom mismatch handler restores automatic recovery.
112
113Call `event.defaultBehavior()` to explicitly run built-in recovery: eager mismatches warn, navigation mismatches reload the destination, fetcher mismatches reload the current document, and imperative mismatches return without reloading. Repeated calls for one shared mismatch event reuse the same promise. It does not invoke other custom handlers or stop their propagation.
114
115```tsx
116unstable_useRouteDiscovery({
117 async onManifestMismatch(event) {
118 if (hasUnsavedChanges) {
119 event.stopPropagation();
120 setShowReloadDialog(true);
121 return;
122 }
123 await event.defaultBehavior();
124 },
125});
126```
127
128Custom handlers run in reverse registration order, with each awaited before the next. `stopPropagation()` prevents earlier handlers from running; it does not complete discovery or allow an operation to continue against an incomplete tree.
129
130```tsx
131unstable_useRouteDiscovery({
132 onManifestMismatch(event) {
133 if (hasUnsavedChanges) {
134 event.stopPropagation();
135 setShowReloadDialog(true);
136 }
137 },
138});
139```
140
141An editor mounted after a root recovery handler can use this to preserve a draft. Unmounting removes its registration and reveals earlier handlers. Callback updates preserve registration priority. This is registration order, not a guarantee that the deepest component runs first.
142
143Eager discovery waits for the initialized route tree to commit so a root handler can intercept the first request. It does not wait for every suspended descendant or guarantee an idle main thread. Discovery needed before handlers can mount retains normal lazy behavior.
144
145Without mismatch handlers, existing automatic recovery for eager/navigation/fetcher discovery remains unchanged. Deployment polling and staleness policy remain application-owned.