T
TanStack2y ago
conscious-sapphire

how to navigate to a route that has search params with default values?

I have the following route:
const searchSchema = z.object({
page: z.number().catch(1),
filter: z.string().catch(""),
});

const route = new Route({
getParentRoute: () => rootRoute,
path: "/route",
validateSearch: searchSchema
}),
component: RouteComponent
});
const searchSchema = z.object({
page: z.number().catch(1),
filter: z.string().catch(""),
});

const route = new Route({
getParentRoute: () => rootRoute,
path: "/route",
validateSearch: searchSchema
}),
component: RouteComponent
});
How would I navigate to this route without specifying the search params? Ideally I could just do this:
navigate({
to: '/route1'
})
navigate({
to: '/route1'
})
I can omit the search params if I use the following zod schema and declare them all to be optional:
const searchSchema = z.object({
page: z.number().optional().catch(1),
filter: z.string().optional().catch(""),
});
const searchSchema = z.object({
page: z.number().optional().catch(1),
filter: z.string().optional().catch(""),
});
However, now the search params types include undefined:
{
page?: number | undefined;
filter?: string | undefined;
}
{
page?: number | undefined;
filter?: string | undefined;
}
But they are never undefined at runtime ...
21 Replies
inland-turquoise
inland-turquoise2y ago
This… Is a wonderful question, that I don’t have an answer to right now
conscious-sapphire
conscious-sapphireOP2y ago
I found out that zod has two types: an input and an output one. unfortunately, catch() loses the type information (it becomes unknown), but we can combine those two types likes this
type MakeOptionalOutput<
Schema extends ZodType,
Input = z.input<Schema>,
Output = z.output<Schema>,
> = {
[K in keyof Input & keyof Output as undefined extends Input[K]
? K
: never]?: Output[K]
}

type MakeStrictInput<
Schema extends ZodType,
Output = z.output<Schema>,
> = MakeOptionalOutput<Schema> &
Omit<Output, keyof MakeOptionalOutput<Schema>>
type MakeOptionalOutput<
Schema extends ZodType,
Input = z.input<Schema>,
Output = z.output<Schema>,
> = {
[K in keyof Input & keyof Output as undefined extends Input[K]
? K
: never]?: Output[K]
}

type MakeStrictInput<
Schema extends ZodType,
Output = z.output<Schema>,
> = MakeOptionalOutput<Schema> &
Omit<Output, keyof MakeOptionalOutput<Schema>>
No description
conscious-sapphire
conscious-sapphireOP2y ago
This results in:
No description
conscious-sapphire
conscious-sapphireOP2y ago
now, how could we use this type in react-router? we could look at the input parameters of SearchSchemaValidator:
export type SearchSchemaValidator<TInput, TReturn> =
| SearchSchemaValidatorObj<TInput, TReturn>
| SearchSchemaValidatorFn<TInput, TReturn>

export type SearchSchemaValidatorObj<TInput, TReturn> = {
parse?: SearchSchemaValidatorFn<TInput, TReturn>
}

export type SearchSchemaValidatorFn<TInput, TReturn> = (
searchObj: TInput,
) => TReturn`
export type SearchSchemaValidator<TInput, TReturn> =
| SearchSchemaValidatorObj<TInput, TReturn>
| SearchSchemaValidatorFn<TInput, TReturn>

export type SearchSchemaValidatorObj<TInput, TReturn> = {
parse?: SearchSchemaValidatorFn<TInput, TReturn>
}

export type SearchSchemaValidatorFn<TInput, TReturn> = (
searchObj: TInput,
) => TReturn`
but how would we know that we want to use TInput instead of TReturn to build up the full search schema? maybe.... if TInput equals unknown, then prefer TReturn over TInput? I toyed around with this idea, but I think automatically detecting the input type is not feasible. Instead, I'd rather make it explicit. how about a @tanstack/router-zod-adapter that provides a wrapper around a zod schema and is passed into validateSearch? this wrapper could look like this:
function wrapSchema<
Output,
Def extends ZodTypeDef,
Input,
StrictInput = MakeStrictInput<ZodType<Output, Def, Input>>,
>(schema: ZodType<Output, Def, Input>) {
return (input: StrictInput & SearchSchemaInput) => schema.parse(input);
}
function wrapSchema<
Output,
Def extends ZodTypeDef,
Input,
StrictInput = MakeStrictInput<ZodType<Output, Def, Input>>,
>(schema: ZodType<Output, Def, Input>) {
return (input: StrictInput & SearchSchemaInput) => schema.parse(input);
}
SearchSchemaInput is a tag that can be used to signalize to react-router that this input type should be used for the search param of <Link> / navigate() I created a PR in my fork repo, since it's based on another not yet merged PR
conscious-sapphire
conscious-sapphireOP2y ago
@Tanner Linsley now that PR#867 is merged, I created the PR against the main repo: https://github.com/TanStack/router/pull/907
conscious-sapphire
conscious-sapphireOP2y ago
let me know what you think about this the symbol thing is probably not necessary since we don't need it at runtime I removed the symbol usage. most interesting line from DX perspective is adding the SearchSchemaInput tag here: https://github.com/TanStack/router/pull/907/files#diff-df27b1564205f3fa16917864857be8e213156d2b8e7e9d7dd6c4a479d215a8fbR149 If you approve of this API, I'll update the documentation
inland-turquoise
inland-turquoise2y ago
This is cool Backwards compatible? I approve pretty sure Unless you have any thing you’re not sure on
conscious-sapphire
conscious-sapphireOP2y ago
it is backwards compatible instead of the "add a tag type" API we could also use a separate function (e.g. validateSearchInputTyped) these are the two solutions I came up with, there might be better ones
correct-apricot
correct-apricot2y ago
Heya, I know this thread is a bit older but I'm having the same issue right now but I'm confused as to how this solution actually works? In my case it seems the input type is completely ignored in the validateSearch callback.. Doesn't matter what type I put there, even with the tagged SearchSchemaInput there is no difference. The search type for navigate and Link are always of the type of the parse output.. Even something like
validateSearch: (search: { definitelyWrong: number } & SearchSchemaInput) =>
productsSearchSchema.parse(search),
validateSearch: (search: { definitelyWrong: number } & SearchSchemaInput) =>
productsSearchSchema.parse(search),
Gets completely ignored, it still expects the inferred productsSearchSchema type.. Is this solution still up to date? Or is it possible I'm doing something wrong?
conscious-sapphire
conscious-sapphireOP2y ago
this should work, please provide a minimal complete example on e.g. codesandbox
correct-apricot
correct-apricot2y ago
Hmm interesting, in a codesandbox fork it works fine. I'll try to check tomorrow why this doesn't work for my project setup.. Might have to update zod while I'm at it. Sorry for the inconvenience! I think I found my issue: My Schema:
const productsSearchSchema = z.object({
...
page: z.number().catch(1),
});
const productsSearchSchema = z.object({
...
page: z.number().catch(1),
});
I was using navigate like so:
const navigate2 = useNavigate({ from: "/products" });
navigate2({ search: {} });
const navigate2 = useNavigate({ from: "/products" });
navigate2({ search: {} });
Here the search property uses the inferred type from the schema where page is required and the search prop shows a type error. But using it like so:
const navigate1 = useNavigate();
navigate1({ to: "/products", search: {} });
const navigate1 = useNavigate();
navigate1({ to: "/products", search: {} });
Works as expected, the search prop uses the input type. --- I'm not sure if this is relevant or needs to be addressed.. For now this would be solved for me and I'll just try to always us the latter approach. My understanding was, that with the from key I could tell the navigate method where it is right now and navigating to itself (without the to prop) it would resolve the types for the /products route. Codesandbox for reproduction: https://codesandbox.io/p/devbox/mutable-framework-9gthk9
conscious-sapphire
conscious-sapphireOP2y ago
GitHub
Release v1.15.21 · TanStack/router
Version 1.15.21 - 2/7/2024, 5:35 PM Changes Fix SearchSchemaInput now also works when only from is specified and to is not (#1161) (8a7805b) by Manuel Schiller Packages @tanstack/react-router@1....
passive-yellow
passive-yellow2y ago
Hi guys. Maybe I don't understand something, but I still don't know why I need to pass search object while navigating to a page which has a search schema with .catch(). That's my schema:
const schema = z.object({
search: z.string().catch("")
})
const schema = z.object({
search: z.string().catch("")
})
and that's my Link:
<Link
to="/history"
// error because 'search' is not provided
/>
<Link
to="/history"
// error because 'search' is not provided
/>
conscious-sapphire
conscious-sapphireOP2y ago
what is the inferred return type of that zod schema?
passive-yellow
passive-yellow2y ago
that's a good point. it's
type ReturnType = {
search: string;
}
type ReturnType = {
search: string;
}
it there an option to create schema which return type would be i.e:
{
search: string | ""
}
{
search: string | ""
}
conscious-sapphire
conscious-sapphireOP2y ago
z.union? but that would not help you with router requiring you to specify the search param i have the same issue in my app, I can paste you a helper function in a few days when I am back at my computer.
passive-yellow
passive-yellow2y ago
that would be great!
conscious-sapphire
conscious-sapphireOP2y ago
import { type Expand, type SearchSchemaInput } from '@tanstack/react-router';
import { type ZodTypeDef, type ZodType, type z } from 'zod';

type MakeOptionalOutput<
Schema extends ZodType,
Input = z.input<Schema>,
Output = z.output<Schema>,
> = {
[K in keyof Input & keyof Output as undefined extends Input[K]
? K
: never]?: Output[K];
};

type MakeStrictInput<
Schema extends ZodType,
Output = z.output<Schema>,
> = MakeOptionalOutput<Schema> & Omit<Output, keyof MakeOptionalOutput<Schema>>;

export function makeInputSchema<
Output,
Def extends ZodTypeDef,
Input,
StrictInput = Expand<MakeStrictInput<ZodType<Output, Def, Input>>>,
>(
schema: ZodType<Output, Def, Input>,
): (input: StrictInput & SearchSchemaInput) => Output {
return (input: StrictInput & SearchSchemaInput) => schema.parse(input);
}
import { type Expand, type SearchSchemaInput } from '@tanstack/react-router';
import { type ZodTypeDef, type ZodType, type z } from 'zod';

type MakeOptionalOutput<
Schema extends ZodType,
Input = z.input<Schema>,
Output = z.output<Schema>,
> = {
[K in keyof Input & keyof Output as undefined extends Input[K]
? K
: never]?: Output[K];
};

type MakeStrictInput<
Schema extends ZodType,
Output = z.output<Schema>,
> = MakeOptionalOutput<Schema> & Omit<Output, keyof MakeOptionalOutput<Schema>>;

export function makeInputSchema<
Output,
Def extends ZodTypeDef,
Input,
StrictInput = Expand<MakeStrictInput<ZodType<Output, Def, Input>>>,
>(
schema: ZodType<Output, Def, Input>,
): (input: StrictInput & SearchSchemaInput) => Output {
return (input: StrictInput & SearchSchemaInput) => schema.parse(input);
}
and I use it like this:
const searchSchemaInput = makeInputSchema(
searchSchema,
);

export const Route = createFileRoute('/foo/$id')({
component: MyComponent,
validateSearch: searchSchemaInput
});
const searchSchemaInput = makeInputSchema(
searchSchema,
);

export const Route = createFileRoute('/foo/$id')({
component: MyComponent,
validateSearch: searchSchemaInput
});
so for your example schema it looks like this:
const schema = z.object({
search: z.string().catch(''),
});

type SchemaType = z.infer<typeof schema>;

/*
type SchemaType = {
search: string;
}
*/

const inputSchema = makeInputSchema(schema);
/*
const inputSchema: (input: {
search?: string | undefined;
} & SearchSchemaInput) => {
search: string;
}
*/
const schema = z.object({
search: z.string().catch(''),
});

type SchemaType = z.infer<typeof schema>;

/*
type SchemaType = {
search: string;
}
*/

const inputSchema = makeInputSchema(schema);
/*
const inputSchema: (input: {
search?: string | undefined;
} & SearchSchemaInput) => {
search: string;
}
*/
passive-yellow
passive-yellow2y ago
That's interesting, thank you! However I lost search types and now its just any and return type of Route.useSearch() is {}
conscious-sapphire
conscious-sapphireOP2y ago
can you please provide a minimal complete example?

Did you find this page helpful?