|
5 | 5 |
|
6 | 6 | New (unstable) `useRoute` hook for accessing data from specific routes |
7 | 7 |
|
8 | | -`useRouteLoaderData` has many shortcomings: |
| 8 | +For example, let's say you have an `admin` route somewhere in your app and you want any child routes of `admin` to all have access to the `loaderData` and `actionData` from `admin.` |
9 | 9 |
|
10 | | -1. Its `routeId` arg is typed as `string`, so TS won't complain if you pass in a non-existent route ID |
11 | | -2. Type-safety was limited to a `typeof loader` generic that required you to manually import and pass in the type for the corresponding `loader` |
12 | | -3. Even with `typeof loader`, the types were not aware of `clientLoader`, `HydrateFallback`, and `clientLoader.hydrate = true`, which all affect the type for `loaderData` |
13 | | -4. It is limited solely to `loader` data, but does not provide `action` data |
14 | | -5. It introduced confusion about when to use `useLoaderData` and when to use `useRouteLoaderData` |
15 | | - |
16 | | -```ts |
| 10 | +```tsx |
17 | 11 | // app/routes/admin.tsx |
18 | | -export const loader = () => ({ message: "Hello, loader!" }); |
| 12 | +import { Outlet } from "react-router"; |
19 | 13 |
|
20 | | -export const action = () => ({ message: "Hello, action!" }); |
21 | | -``` |
| 14 | +export const loader = () => ({ message: "Hello, loader!" }); |
22 | 15 |
|
23 | | -```ts |
24 | | -import { type loader } from "../routes/admin"; |
| 16 | +export const action = () => ({ count: 1 }); |
25 | 17 |
|
26 | | -export function Widget() { |
27 | | - const loaderData = useRouteLoaderData<typeof loader>("routes/admin"); |
28 | | - // ... |
| 18 | +export default function Component() { |
| 19 | + return ( |
| 20 | + <div> |
| 21 | + {/* ... */} |
| 22 | + <Outlet /> |
| 23 | + {/* ... */} |
| 24 | + </div> |
| 25 | + ); |
29 | 26 | } |
30 | 27 | ``` |
31 | 28 |
|
32 | | -With `useRoute`, all of these concerns have been fixed: |
| 29 | +You might even want to create a reusable widget that all of the routes nested under `admin` could use: |
33 | 30 |
|
34 | | -```ts |
| 31 | +```tsx |
35 | 32 | import { unstable_useRoute as useRoute } from "react-router"; |
36 | 33 |
|
37 | | -export function Widget() { |
38 | | - const admin = useRoute("routes/admin"); |
39 | | - console.log(admin?.loaderData?.message); |
40 | | - console.log(admin?.actionData?.message); |
41 | | - // ... |
| 34 | +export function AdminWidget() { |
| 35 | + // How to get `message` and `count` from `admin` route? |
42 | 36 | } |
43 | 37 | ``` |
44 | 38 |
|
45 | | -Note: `useRoute` returns `undefined` if the route is not part of the current page. |
| 39 | +In framework mode, `useRoute` knows all your app's routes and gives you TS errors when invalid route IDs are passed in: |
| 40 | + |
| 41 | +```tsx |
| 42 | +export function AdminWidget() { |
| 43 | + const admin = useRoute("routes/dmin"); |
| 44 | + // ^^^^^^^^^^^ |
| 45 | +} |
| 46 | +``` |
46 | 47 |
|
47 | | -The `root` route is special because it is guaranteed to be part of the current page, so no need to use `?.` or any other `undefined` checks: |
| 48 | +`useRoute` returns `undefined` if the route is not part of the current page: |
48 | 49 |
|
49 | | -```ts |
50 | | -export function Widget() { |
51 | | - const root = useRoute("root"); |
52 | | - console.log(root.loaderData?.message, root.actionData?.message); |
53 | | - // ... |
| 50 | +```tsx |
| 51 | +export function AdminWidget() { |
| 52 | + const admin = useRoute("routes/admin"); |
| 53 | + if (!admin) { |
| 54 | + throw new Error(`AdminWidget used outside of "routes/admin"`); |
| 55 | + } |
54 | 56 | } |
55 | 57 | ``` |
56 | 58 |
|
57 | | -You may have noticed that `loaderData` and `actionData` are marked as optional. |
58 | | -This is intentional as there's no guarantee that `loaderData` nor `actionData` exists when called in certain contexts like within an `ErrorBoundary`: |
| 59 | +Note: the `root` route is the exception since it is guaranteed to be part of the current page. |
| 60 | +As a result, `useRoute` never returns `undefined` for `root`. |
59 | 61 |
|
60 | | -```ts |
61 | | -export function ErrorBoundary() { |
| 62 | +`loaderData` and `actionData` are marked as optional since they could be accessed before the `action` is triggered or after the `loader` threw an error: |
| 63 | + |
| 64 | +```tsx |
| 65 | +export function AdminWidget() { |
62 | 66 | const admin = useRoute("routes/admin"); |
63 | | - console.log(admin?.loaderData?.message); |
64 | | - // ^^ |
65 | | - // `loader` for itself could have thrown an error, |
66 | | - // so you need to check if `loaderData` exists! |
| 67 | + if (!admin) { |
| 68 | + throw new Error(`AdminWidget used outside of "routes/admin"`); |
| 69 | + } |
| 70 | + const { loaderData, actionData } = admin; |
| 71 | + console.log(loaderData); |
| 72 | + // ^? { message: string } | undefined |
| 73 | + console.log(actionData); |
| 74 | + // ^? { count: number } | undefined |
67 | 75 | } |
68 | 76 | ``` |
69 | 77 |
|
70 | | -In an effort to consolidate on fewer, more intuitive hooks, `useRoute` can be called without arguments as a replacement for `useLoaderData` and `useActionData`: |
| 78 | +If instead of a specific route, you wanted access to the _current_ route's `loaderData` and `actionData`, you can call `useRoute` without arguments: |
71 | 79 |
|
72 | | -```ts |
73 | | -export function Widget() { |
| 80 | +```tsx |
| 81 | +export function AdminWidget() { |
74 | 82 | const currentRoute = useRoute(); |
75 | 83 | currentRoute.loaderData; |
76 | 84 | currentRoute.actionData; |
77 | 85 | } |
78 | 86 | ``` |
79 | 87 |
|
80 | | -Since `Widget` is a reusable component that could be within any route, we have no guarantees about the types for `loaderData` nor `actionData`. |
81 | | -As a result, they are both typed as `unknown` and it is up to you to narrow the type to what your reusable component needs (for example, via `zod`). |
| 88 | +This usage is equivalent to calling `useLoaderData` and `useActionData`, but consolidates all route data access into one hook: `useRoute`. |
| 89 | + |
| 90 | +Note: when calling `useRoute()` (without a route ID), TS has no way to know which route is the current route. |
| 91 | +As a result, `loaderData` and `actionData` are typed as `unknown`. |
| 92 | +If you want more type-safety, you can either narrow the type yourself with something like `zod` or you can refactor your app to pass down typed props to your `AdminWidget`: |
| 93 | + |
| 94 | +```tsx |
| 95 | +export function AdminWidget({ |
| 96 | + message, |
| 97 | + count, |
| 98 | +}: { |
| 99 | + message: string; |
| 100 | + count: number; |
| 101 | +}) { |
| 102 | + /* ... */ |
| 103 | +} |
| 104 | +``` |
0 commit comments