Skip to content

Commit 8def11a

Browse files
committed
Document standard schema support
1 parent 4065a14 commit 8def11a

File tree

2 files changed

+247
-19
lines changed

2 files changed

+247
-19
lines changed

versioned_docs/version-8.x/configuring-links.md

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,13 @@ By default, query params are parsed to get the params for a screen. For example,
457457

458458
You can also customize how the params are parsed from the URL. Let's say you want the URL to look like `/user/jane` where the `id` param is `jane` instead of having the `id` in query params. You can do this by specifying `user/:id` for the `path`. **When the path segment starts with `:`, it'll be treated as a param**. For example, the URL `/user/jane` would resolve to `Profile` screen with the string `jane` as a value of the `id` param and will be available in `route.params.id` in `Profile` screen.
459459

460-
By default, all params are treated as strings. You can also customize how to parse them by specifying a function in the `parse` property to parse the param, and a function in the `stringify` property to convert it back to a string.
460+
By default, all params are parsed as strings. You can customize how to parse them by specifying a function or a [Standard Schema](https://standardschema.dev/) in the `parse` property, and a function in the `stringify` property to convert it back to a string.
461461

462-
If you wanted to resolve `/user/@jane/settings` to result in the params `{ id: 'jane' section: 'settings' }`, you could make `Profile`'s config to look like this:
462+
### Using functions
463+
464+
The `parse` property accepts a function that receives the string value from the URL and returns the parsed value. The `stringify` property accepts a function to convert the param back to a string.
465+
466+
For example, to resolve `/user/@jane/settings` to the params `{ id: 'jane', section: 'settings' }`, you could use `parse` to strip the `@` prefix and `stringify` to add it back:
463467

464468
<Tabs groupId="config" queryString="config">
465469
<TabItem value="static" label="Static" default>
@@ -527,6 +531,135 @@ const state = {
527531

528532
</details>
529533

534+
### Using Standard Schema
535+
536+
The `parse` property also accepts a schema from a [Standard Schema](https://standardschema.dev/) compatible library such as [Zod](https://zod.dev/), [Valibot](https://valibot.dev/) or [ArkType](https://arktype.io/), which provides both parsing and validation in one step. The `stringify` property still accepts a function to convert the param back to a string.
537+
538+
If the schema validation fails, the URL won't match the current screen and React Navigation will try the next matching config. This lets you use schemas to narrow down which screen handles a URL.
539+
540+
<Tabs groupId="config" queryString="config">
541+
<TabItem value="static" label="Static" default>
542+
543+
```js
544+
import { z } from 'zod';
545+
546+
const RootStack = createStackNavigator({
547+
screens: {
548+
Profile: {
549+
screen: ProfileScreen,
550+
// highlight-start
551+
linking: {
552+
path: 'user/:id',
553+
parse: {
554+
id: z.string().startsWith('@'),
555+
},
556+
},
557+
// highlight-end
558+
},
559+
},
560+
});
561+
```
562+
563+
</TabItem>
564+
<TabItem value="dynamic" label="Dynamic">
565+
566+
```js
567+
import { z } from 'zod';
568+
569+
const config = {
570+
screens: {
571+
Profile: {
572+
// highlight-start
573+
path: 'user/:id',
574+
parse: {
575+
id: z.string().startsWith('@'),
576+
},
577+
// highlight-end
578+
},
579+
},
580+
};
581+
```
582+
583+
</TabItem>
584+
</Tabs>
585+
586+
In this example, the `Profile` screen will only match if the `id` param starts with `@`. If the URL is `/user/jane`, it won't match because the schema validation fails, and React Navigation will try the next config.
587+
588+
Here's another example that transforms a date string from the URL into a timestamp:
589+
590+
<Tabs groupId="config" queryString="config">
591+
<TabItem value="static" label="Static" default>
592+
593+
```js
594+
import * as v from 'valibot';
595+
596+
const RootStack = createStackNavigator({
597+
screens: {
598+
Chat: {
599+
screen: ChatScreen,
600+
// highlight-start
601+
linking: {
602+
path: 'chat/:date',
603+
parse: {
604+
date: v.pipe(
605+
v.string(),
606+
v.transform((input) => new Date(input).getTime()),
607+
v.number()
608+
),
609+
},
610+
stringify: {
611+
date: (date) => {
612+
const d = new Date(date);
613+
614+
return d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate();
615+
},
616+
},
617+
},
618+
// highlight-end
619+
},
620+
},
621+
});
622+
```
623+
624+
</TabItem>
625+
<TabItem value="dynamic" label="Dynamic">
626+
627+
```js
628+
import * as v from 'valibot';
629+
630+
const config = {
631+
screens: {
632+
Chat: {
633+
// highlight-start
634+
path: 'chat/:date',
635+
parse: {
636+
date: v.pipe(
637+
v.string(),
638+
v.transform((input) => new Date(input).getTime()),
639+
v.number()
640+
),
641+
},
642+
stringify: {
643+
date: (date) => {
644+
const d = new Date(date);
645+
646+
return d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate();
647+
},
648+
},
649+
// highlight-end
650+
},
651+
},
652+
};
653+
```
654+
655+
</TabItem>
656+
</Tabs>
657+
658+
Using Standard Schema has a few advantages over using functions for parsing:
659+
660+
- **Support for validation and fallback**: A parse function only parses the param. A schema can also validate the param. If the validation fails, the URL won't match the current screen and React Navigation will try the next matching config. This lets you use schemas to narrow down which screen handles a URL. Schemas are also called with `undefined` when a query param is missing, which lets them provide a fallback, while parse functions are not called when a query param is missing.
661+
- **Better Query Param handling with TypeScript**: When using [Static Configuration](static-configuration.md), query params (e.g. `?foo=bar`) are always inferred as optional with `parse` functions. With schemas, you can specify whether a query param is required (e.g. `z.string()`) or optional (e.g. `z.string().optional()`). See [Parse function vs Standard Schema](typescript.md#parse-function-vs-standard-schema) for more details.
662+
530663
## Marking params as optional
531664

532665
Sometimes a param may or may not be present in the URL depending on certain conditions. For example, in the above scenario, you may not always have the section parameter in the URL, i.e. both `/user/jane/settings` and `/user/jane` should go to the `Profile` screen, but the `section` param (with the value `settings` in this case) may or may not be present.
@@ -1129,7 +1262,7 @@ const config = {
11291262
11301263
## Serializing and parsing params
11311264
1132-
Since URLs are strings, any params you have for routes are also converted to strings when constructing the path.
1265+
Since URLs are strings, any params you have for routes are also converted to strings when constructing the path. You can customize parsing with [functions](#using-functions) or [Standard Schemas](#using-standard-schema).
11331266
11341267
For example, say you have the URL `/chat/1589842744264` with the following config:
11351268

versioned_docs/version-8.x/typescript.md

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,36 @@ After setting up the type for the root navigator, all we need to do is specify t
5757

5858
This can be done in 2 ways:
5959

60-
1. The path pattern specified in the linking config (e.g. for `path: 'profile/:userId'`, the type of `route.params` is `{ userId: string }`). The type can be further customized by using a [`parse` function in the linking config](configuring-links.md#passing-params):
60+
1. The path pattern specified in the linking config (e.g. for `path: 'profile/:userId'`, the type of `route.params` is `{ userId: string }`). The type can be further customized by:
61+
- Using a `parse` function:
62+
63+
```ts
64+
linking: {
65+
// highlight-start
66+
path: 'profile/:userId',
67+
parse: {
68+
userId: (id) => parseInt(id, 10),
69+
},
70+
// highlight-end
71+
},
72+
```
6173

62-
```ts
63-
linking: {
64-
// highlight-start
65-
path: 'profile/:userId',
66-
parse: {
67-
userId: (id) => parseInt(id, 10),
74+
- Using a Standard Schema:
75+
76+
```ts
77+
import { z } from 'zod';
78+
79+
linking: {
80+
// highlight-start
81+
path: 'profile/:userId',
82+
parse: {
83+
userId: z.coerce.number(),
84+
},
85+
// highlight-end
6886
},
69-
// highlight-end
70-
},
71-
```
87+
```
7288

73-
The above example would make the type of `route.params` be `{ userId: number }` since the `parse` function converts the string from the URL to a number.
89+
The above examples would also make the type of `route.params` be `{ userId: number }`. See [passing params](configuring-links.md#passing-params) for the API and [Parse function vs Standard Schema](#parse-function-vs-standard-schema) for the differences in type inference.
7490
7591
This is the recommended way to specify params for screens that are accessible via deep linking or if your app runs on the Web, as it ensures that the types of params are consistent with the URL.
7692
@@ -102,10 +118,6 @@ This can be done in 2 ways:
102118
}
103119
```
104120

105-
The above example would make the type of `route.params` be `{ userId: number }` since the `parse` function converts the string from the URL to a number.
106-
107-
If your app supports deep linking or runs on the Web, you can use this pattern to specify any additional optional params that don't appear in the path pattern (e.g. query params). Make sure to add `| undefined` to the type of params to make them optional, as query params may not be present in the URL.
108-
109121
If both `screen` and `linking` specify params, the final type of `route.params` is the intersection of both types.
110122

111123
This is how the complete example would look like:
@@ -128,7 +140,29 @@ const MyStack = createNativeStackNavigator({
128140
});
129141
```
130142

131-
If you have specified the params in `linking`, it's recommended to not specify them again in the component's props, and use `useRoute('ScreenName')` instead to get the correctly typed `route` object.
143+
Or with a Standard Schema:
144+
145+
```ts
146+
import { z } from 'zod';
147+
148+
const MyStack = createNativeStackNavigator({
149+
screens: {
150+
// highlight-start
151+
Profile: createNativeStackScreen({
152+
screen: ProfileScreen,
153+
linking: {
154+
path: 'profile/:userId',
155+
parse: {
156+
userId: z.coerce.number(),
157+
},
158+
},
159+
}),
160+
// highlight-end
161+
},
162+
});
163+
```
164+
165+
If you have specified the params in `linking`, it's recommended to not specify them again in the component's props, and use [`useRoute('ScreenName')`](#using-typed-hooks) instead to get the correctly typed `route` object.
132166

133167
The `createXScreen` helper functions enable type inference in screen configuration callbacks like `options`, `listeners`, etc. Each navigator exports its own version of the helper function:
134168

@@ -140,6 +174,67 @@ The `createXScreen` helper functions enable type inference in screen configurati
140174

141175
See [Static configuration](static-configuration.md#createxscreen) for more details.
142176

177+
## Parse function vs Standard Schema
178+
179+
Both parse functions and Standard Schemas infer param types from the `parse` config, but they differ in how they handle type inference for query params.
180+
181+
### Path pattern params
182+
183+
For params in path pattern, both approaches work the same way:
184+
185+
- The return type of the function or the output type of the schema is used as the param type.
186+
- If the pattern includes the `?` suffix, it's inferred as optional.
187+
188+
e.g. both of the following configs would make the type of `route.params` be `{ id: number }`:
189+
190+
Parse function:
191+
192+
```ts
193+
parse: {
194+
id: Number,
195+
}
196+
```
197+
198+
Standard Schema:
199+
200+
```ts
201+
parse: {
202+
id: z.coerce.number(),
203+
}
204+
```
205+
206+
### Query params
207+
208+
Query params are inferred differently based on whether you use a parse function or a Standard Schema.
209+
210+
- With a parse function, query params are always inferred as optional since they may not be present in the URL:
211+
212+
```ts
213+
parse: {
214+
sort: (value: string) => (value === 'new' ? 'new' : 'top'),
215+
}
216+
```
217+
218+
Here `route.params` are inferred as `{ sort?: 'new' | 'top' }`.
219+
220+
- With a Standard Schema, query params are inferred as required or optional based on the schema's output type:
221+
222+
```ts
223+
parse: {
224+
sort: z.string(),
225+
}
226+
```
227+
228+
Here `route.params` are inferred as `{ sort: string }`.
229+
230+
```ts
231+
parse: {
232+
sort: z.string().optional(),
233+
}
234+
```
235+
236+
Here `route.params` are inferred as `{ sort?: string | undefined }`.
237+
143238
## Using typed hooks
144239

145240
The [`useRoute`](use-route.md), [`useNavigation`](use-navigation.md), and [`useNavigationState`](use-navigation-state.md) hooks accept the name of the current screen or any parent screen where it's nested as an argument to infer the correct types.

0 commit comments

Comments
 (0)