Skip to content

Commit

Permalink
feat(react-router): params.parse and params.stringify instead of …
Browse files Browse the repository at this point in the history
…`parseParams` and `stringifyParams` (#1826)
  • Loading branch information
chorobin authored Jun 28, 2024
1 parent 099d423 commit 1d733f6
Show file tree
Hide file tree
Showing 14 changed files with 562 additions and 96 deletions.
15 changes: 13 additions & 2 deletions docs/framework/react/api/router/RouteOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,29 @@ The `RouteOptions` type accepts an object with the following properties:
- A function that will be called when this route is matched and passed the raw search params from the current location and return valid parsed search params. If this function throws, the route will be put into an error state and the error will be thrown during render. If this function does not throw, its return value will be used as the route's search params and the return type will be inferred into the rest of the router.
- Optionally, the parameter type can be tagged with the `SearchSchemaInput` type like this: `(searchParams: TSearchSchemaInput & SearchSchemaInput) => TSearchSchema`. If this tag is present, `TSearchSchemaInput` will be used to type the `search` property of `<Link />` and `navigate()` **instead of** `TSearchSchema`. The difference between `TSearchSchemaInput` and `TSearchSchema` can be useful, for example, to express optional search parameters.

### `parseParams` method
### `parseParams` method (⚠️ deprecated)

- Type: `(rawParams: Record<string, string>) => TParams`
- Optional
- A function that will be called when this route is matched and passed the raw params from the current location and return valid parsed params. If this function throws, the route will be put into an error state and the error will be thrown during render. If this function does not throw, its return value will be used as the route's params and the return type will be inferred into the rest of the router.

### `stringifyParams` method
### `stringifyParams` method (⚠️ deprecated)

- Type: `(params: TParams) => Record<string, string>`
- Required if `parseParams` is provided
- A function that will be called when this routes parsed params are being used to build a location. This function should return a valid object of `Record<string, string>` mapping.

### `params.parse` method

- Type: `(rawParams: Record<string, string>) => TParams`
- Optional
- A function that will be called when this route is matched and passed the raw params from the current location and return valid parsed params. If this function throws, the route will be put into an error state and the error will be thrown during render. If this function does not throw, its return value will be used as the route's params and the return type will be inferred into the rest of the router.

### `params.stringify` method

- Type: `(params: TParams) => Record<string, string>`
- A function that will be called when this routes parsed params are being used to build a location. This function should return a valid object of `Record<string, string>` mapping.

### `beforeLoad` method

- Type:
Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/guide/authenticated-routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The `route.beforeLoad` option allows you to specify a function that will be call
The `beforeLoad` function runs in relative order to these other route loading functions:

- Route Matching (Top-Down)
- `route.parseParams`
- `route.params.parse`
- `route.validateSearch`
- Route Loading (including Preloading)
- **`route.beforeLoad`**
Expand Down
2 changes: 1 addition & 1 deletion docs/framework/react/guide/data-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Beyond these normal expectations of a router, TanStack Router goes above and bey
Every time a URL/history update is detected, the router executes the following sequence:

- Route Matching (Top-Down)
- `route.parseParams`
- `route.params.parse`
- `route.validateSearch`
- Route Pre-Loading (Serial)
- `route.beforeLoad`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { useMutation } from '../hooks/useMutation'
import { fetchInvoiceById, patchInvoice } from '../utils/mockTodos'

export const Route = createFileRoute('/dashboard/invoices/$invoiceId')({
parseParams: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringifyParams: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
params: {
parse: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
},
validateSearch: (search) =>
z
.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
} from '../utils/queryOptions'

export const Route = createFileRoute('/dashboard/invoices/$invoiceId')({
parseParams: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringifyParams: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
params: {
parse: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
},
validateSearch: (search) =>
z
.object({
Expand Down
10 changes: 6 additions & 4 deletions examples/react/kitchen-sink-react-query/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,12 @@ function InvoicesIndexComponent() {
const invoiceRoute = createRoute({
getParentRoute: () => invoicesRoute,
path: '$invoiceId',
parseParams: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringifyParams: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
params: {
parse: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
},
validateSearch: (search) =>
z
.object({
Expand Down
10 changes: 6 additions & 4 deletions examples/react/kitchen-sink/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,12 @@ function InvoicesIndexComponent() {
const invoiceRoute = createRoute({
getParentRoute: () => invoicesRoute,
path: '$invoiceId',
parseParams: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringifyParams: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
params: {
parse: (params) => ({
invoiceId: z.number().int().parse(Number(params.invoiceId)),
}),
stringify: ({ invoiceId }) => ({ invoiceId: `${invoiceId}` }),
},
validateSearch: (search) =>
z
.object({
Expand Down
10 changes: 6 additions & 4 deletions examples/react/with-trpc/client/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,12 @@ const postsIndexRoute = createRoute({
const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
parseParams: (params) => ({
postId: z.number().int().parse(Number(params.postId)),
}),
stringifyParams: ({ postId }) => ({ postId: `${postId}` }),
params: {
parse: (params) => ({
postId: z.number().int().parse(Number(params.postId)),
}),
stringify: ({ postId }) => ({ postId: `${postId}` }),
},
validateSearch: z.object({
showNotes: z.boolean().optional(),
notes: z.string().optional(),
Expand Down
3 changes: 0 additions & 3 deletions packages/react-router/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,13 @@ export {
type StaticDataRouteOption,
type RoutePathOptionsIntersection,
type RouteOptions,
type ParamsFallback,
type FileBaseRouteOptions,
type BaseRouteOptions,
type UpdatableRouteOptions,
type UpdatableStaticRouteOption,
type MetaDescriptor,
type RouteLinkEntry,
type ParseParamsOption,
type ParseParamsFn,
type ParseParamsObj,
type SearchSchemaValidator,
type SearchSchemaValidatorObj,
type SearchSchemaValidatorFn,
Expand Down
124 changes: 60 additions & 64 deletions packages/react-router/src/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,25 +91,53 @@ export type RouteOptions<
NoInfer<TLoaderDeps>
>

export type ParamsFallback<
TPath extends string,
TParams,
> = unknown extends TParams ? Record<ParsePathParams<TPath>, string> : TParams
export type ParseParamsFn<TPath extends string, TParams> = (
rawParams: Record<ParsePathParams<TPath>, string>,
) => TParams extends Record<ParsePathParams<TPath>, any>
? TParams
: Record<ParsePathParams<TPath>, any>

export type StringifyParamsFn<TPath extends string, TParams> = (
params: TParams,
) => Record<ParsePathParams<TPath>, string>

export type ParamsOptions<TPath extends string, TParams> = {
params?: {
parse: ParseParamsFn<TPath, TParams>
stringify: StringifyParamsFn<TPath, TParams>
}

/**
@deprecated Use params.parse instead
*/
parseParams?: ParseParamsFn<TPath, TParams>

/**
@deprecated Use params.stringify instead
*/
stringifyParams?: StringifyParamsFn<TPath, TParams>
}

export interface FullSearchSchemaOption<TFullSearchSchema> {
search: TFullSearchSchema
}

export type FileBaseRouteOptions<
TPath extends string = string,
TSearchSchemaInput = Record<string, unknown>,
TSearchSchema = {},
TFullSearchSchema = TSearchSchema,
TParams = {},
TAllParams = ParamsFallback<TPath, TParams>,
TAllParams = {},
TRouteContextReturn = RouteContext,
TParentAllContext = AnyContext,
TAllContext = AnyContext,
TLoaderDeps extends Record<string, any> = {},
TLoaderDataReturn = {},
> = {
validateSearch?: SearchSchemaValidator<TSearchSchemaInput, TSearchSchema>
validateSearch?:
| ((input: TSearchSchemaInput) => TSearchSchema)
| { parse: (input: TSearchSchemaInput) => TSearchSchema }
shouldReload?:
| boolean
| ((
Expand All @@ -119,36 +147,14 @@ export type FileBaseRouteOptions<
// If an error is thrown here, the route's loader will not be called.
// If thrown during a navigation, the navigation will be cancelled and the error will be passed to the `onError` function.
// If thrown during a preload event, the error will be logged to the console.
beforeLoad?: BeforeLoadFn<
TFullSearchSchema,
TAllParams,
TRouteContextReturn,
TParentAllContext
>
loaderDeps?: (opts: { search: TFullSearchSchema }) => TLoaderDeps
loader?: RouteLoaderFn<
TAllParams,
NoInfer<TLoaderDeps>,
NoInfer<TAllContext>,
TLoaderDataReturn
>
} & (
| {
// Both or none
parseParams?: (
rawParams: IsAny<TPath, any, Record<ParsePathParams<TPath>, string>>,
) => TParams extends Record<ParsePathParams<TPath>, any>
? TParams
: 'parseParams must return an object'
stringifyParams?: (
params: NoInfer<ParamsFallback<TPath, TParams>>,
) => Record<ParsePathParams<TPath>, string>
}
| {
stringifyParams?: never
parseParams?: never
}
)
beforeLoad?: (
ctx: BeforeLoadContext<TFullSearchSchema, TAllParams, TParentAllContext>,
) => Promise<TRouteContextReturn> | TRouteContextReturn | void
loaderDeps?: (opts: FullSearchSchemaOption<TFullSearchSchema>) => TLoaderDeps
loader?: (
ctx: LoaderFnContext<TAllParams, TLoaderDeps, TAllContext>,
) => TLoaderDataReturn | Promise<TLoaderDataReturn>
} & ParamsOptions<TPath, TParams>

export type BaseRouteOptions<
TParentRoute extends AnyRoute = AnyRoute,
Expand All @@ -158,7 +164,7 @@ export type BaseRouteOptions<
TSearchSchema = {},
TFullSearchSchema = TSearchSchema,
TParams = {},
TAllParams = ParamsFallback<TPath, TParams>,
TAllParams = {},
TRouteContextReturn = RouteContext,
TParentAllContext = AnyContext,
TAllContext = AnyContext,
Expand All @@ -181,16 +187,14 @@ export type BaseRouteOptions<
getParentRoute: () => TParentRoute
}

type BeforeLoadFn<
in out TFullSearchSchema,
in out TAllParams,
TRouteContextReturn,
in out TParentAllContext,
> = (opts: {
search: TFullSearchSchema
export interface BeforeLoadContext<
TFullSearchSchema,
TAllParams,
TParentAllContext,
> extends FullSearchSchemaOption<TFullSearchSchema> {
abortController: AbortController
preload: boolean
params: TAllParams
params: Expand<TAllParams>
context: TParentAllContext
location: ParsedLocation
/**
Expand All @@ -199,7 +203,7 @@ type BeforeLoadFn<
navigate: NavigateFn
buildLocation: BuildLocationFn
cause: 'preload' | 'enter' | 'stay'
}) => Promise<TRouteContextReturn> | TRouteContextReturn | void
}

export type UpdatableRouteOptions<
TRouteId,
Expand Down Expand Up @@ -286,21 +290,6 @@ type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray

export type RouteLinkEntry = {}

export type ParseParamsOption<TPath extends string, TParams> = ParseParamsFn<
TPath,
TParams
>

export type ParseParamsFn<TPath extends string, TParams> = (
rawParams: IsAny<TPath, any, Record<ParsePathParams<TPath>, string>>,
) => TParams extends Record<ParsePathParams<TPath>, any>
? TParams
: 'parseParams must return an object'

export type ParseParamsObj<TPath extends string, TParams> = {
parse?: ParseParamsFn<TPath, TParams>
}

// The parse type here allows a zod schema to be passed directly to the validator
export type SearchSchemaValidator<TInput, TReturn> =
| SearchSchemaValidatorObj<TInput, TReturn>
Expand All @@ -321,7 +310,7 @@ export type RouteLoaderFn<
TLoaderData = undefined,
> = (
match: LoaderFnContext<TAllParams, TLoaderDeps, TAllContext>,
) => Promise<TLoaderData> | TLoaderData
) => TLoaderData | Promise<TLoaderData>

export interface LoaderFnContext<
in out TAllParams = {},
Expand All @@ -330,7 +319,7 @@ export interface LoaderFnContext<
> {
abortController: AbortController
preload: boolean
params: TAllParams
params: Expand<TAllParams>
deps: TLoaderDeps
context: TAllContext
location: ParsedLocation // Do not supply search schema here so as to demotivate people from trying to shortcut loaderDeps
Expand Down Expand Up @@ -992,6 +981,7 @@ export type RootRouteOptions<
| 'caseSensitive'
| 'parseParams'
| 'stringifyParams'
| 'params'
>

export function createRootRouteWithContext<TRouterContext extends {}>() {
Expand Down Expand Up @@ -1126,6 +1116,7 @@ export function createRootRoute<
| 'caseSensitive'
| 'parseParams'
| 'stringifyParams'
| 'params'
>,
) {
return new RootRoute<
Expand Down Expand Up @@ -1294,7 +1285,12 @@ export class NotFoundRoute<
TLoaderDataReturn,
TLoaderData
>,
'caseSensitive' | 'parseParams' | 'stringifyParams' | 'path' | 'id'
| 'caseSensitive'
| 'parseParams'
| 'stringifyParams'
| 'path'
| 'id'
| 'params'
>,
) {
super({
Expand Down
14 changes: 11 additions & 3 deletions packages/react-router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,9 +934,12 @@ export class Router<
const parseErrors = matchedRoutes.map((route) => {
let parsedParamsError

if (route.options.parseParams) {
const parseParams =
route.options.params?.parse ?? route.options.parseParams

if (parseParams) {
try {
const parsedParams = route.options.parseParams(routeParams)
const parsedParams = parseParams(routeParams)
// Add the parsed params to the accumulated params bag
Object.assign(routeParams, parsedParams)
} catch (err: any) {
Expand Down Expand Up @@ -1193,7 +1196,12 @@ export class Router<

if (Object.keys(nextParams).length > 0) {
matches
?.map((d) => this.looseRoutesById[d.routeId]!.options.stringifyParams)
?.map((d) => {
const route = this.looseRoutesById[d.routeId]
return (
route?.options.params?.stringify ?? route!.options.stringifyParams
)
})
.filter(Boolean)
.forEach((fn) => {
nextParams = { ...nextParams!, ...fn!(nextParams) }
Expand Down
Loading

0 comments on commit 1d733f6

Please sign in to comment.