Replies: 13 comments 20 replies
|
Is it possible to make this feature support multiple validators via TypeSchema? |
|
Re:questions
|
|
1: read+write General: i like the feature. Curious: Did you compare this with how Tanstack Router ensures value safety? |
|
I'm exploring an alternative API: adding the validation method as an option. const diceSchema = z.number().integer().min(1).max(6);
const [dice, roll] = useQueryState(
'dice',
parseAsInteger.withOptions({
validate: diceSchema.parse
})
)It might not make much difference, but passing it this way allows for a few things:
One issue with this approach is that it makes the |
|
With this feature, would it be possible to pass an entire zod object to Passing an entire zod object would help with reusability because you probably use that same zod elsewhere (e.g. to validate function params). A practical example would be using one zod object to capture every param (pagination, column filters, sort, ...) for a (tanstack) table. |
|
In working on this feature, would it benefit to accept any validation library which follows the new Standard Schema spec? |
|
It would be really nice if there were a way to type/validate searchParams on a server Page or Layout component. This would remove the need to manually write If a route doesn't match that component it could throw a 404 or 500. Could lead to more rigid routes and less manual typing. |
|
I went ahead and implemented my own solution to this problem. I created a wrapper called import { inferParserType, ParserMap, useQueryStates } from 'nuqs';
import { createLoader, createSerializer, LoaderInput, Options } from 'nuqs/server';
import { z } from 'zod/v4';
export function createSearchParams<
const Schema extends z.ZodTypeAny,
const Parser extends ParserMap,
>(schema: Schema, parser: Parser) {
const loadSearchParams = createLoader(parser);
return {
schema,
parser,
useQueryStates: (options?: Options) => {
// could add extra stuff to this hook like validation
const qs = useQueryStates(parser, options);
const validateSearchParams = (obj: z.infer<Schema>): boolean => {
try {
schema.parse(obj);
return true;
} catch (err) {
console.error('Error parsing future validation', err);
return false;
}
};
return [...qs, validateSearchParams] as const;
},
parseAndValidate: async (searchParams: Promise<LoaderInput>) => {
const parsed = await loadSearchParams(searchParams);
const result = schema.safeParse(parsed);
if (result.error) {
const error = new Error();
error.cause = z.prettifyError(result.error);
throw error;
}
return parsed;
},
buildUrl: (pathname: string, nextSearchParams: Partial<inferParserType<Parser>>) => {
const serialize = createSerializer(parser);
const params = serialize(nextSearchParams as any); // not sure the typing here yet
return [pathname, params].join('');
},
};
}
export function combineSearchParams<
const T extends readonly ReturnType<typeof createSearchParams>[],
>(...params: T) {
const baseSchema = params.reduce((acc, p) => acc.and(p.schema), z.object({}));
const baseParser = Object.assign({}, ...params.map((p) => p.parser));
return createSearchParams(baseSchema, baseParser);
}To use this, you just setup a zod schema and a nuqs parser in an isomorphic file, e.g. import { createSearchParams } from '@/lib/createSearchParams';
import { parseAsInteger } from 'nuqs/server';
import { z } from 'zod/v4';
export const paginationSchema = z.object({
page: z.int().min(1),
pageSize: z.int().min(1).max(100).nullable(),
});
export const paginationParser = {
page: parseAsInteger.withDefault(1),
pageSize: parseAsInteger.withDefault(20),
};
export const paginationSearchParamsLoader = createSearchParams(paginationSchema, paginationParser);And in your server components you can: const Explore = async ({ searchParams }: PropsWithSearchParams) => {
const { page } = await paginationSearchParamsLoader.parseAndValidate(searchParams);
// page is fully typed and validated, will throw an error if it fails zod validation
};And in your client components you can: const Thread = ({ data, id, children }) => {
const [searchParams, setSearchParams] = paginationSearchParamsLoader.useQueryStates({
shallow: false,
});
// searchParams and setSearchParams are fully typed
});The big advantage here is you no longer have to pass around your schemas/parsers into each component. You basically create a loader function that works on both the client and the server. I've migrated most of my app to this new pattern and I find it rather delightful and rigid. In doing so, my code has become a lot simpler to reason about, more logical, and profoundly cleaner. The caveat is that next.js has no way to pass a server-side error to the client-side error.ts or global-error.ts, which is kind of a pain. Once that is possible, the zod validation is all the more valuable as you can then create an error message for the client to understand why the URL didn't pass validation. At the end of the day, the zod schema is less valuable than I initially thought it would be. Nuqs carries a lot of weight and actually solves most of the problems. Curious if anyone finds this useful - please feel free to play with it or change it. I'd love to see some new patterns develop. Thanks to @franky47 for taking a look at this and providing feedback. 👍 |
|
Hey! I wanted to share our setup that we use at https://tilda-geo.de/regionen/radinfra?map=14.8/52.4713/13.4426&config=pdqyyt.7h39.16g9vk&v=2 … but never found the time to read the thread and join the conversation.
The core part of our migration is our map config. We store all the active layers and in a URL state version: The most magic happens in the This complex setup is used only for the map state that describes which categories/layers are active. For other states like notes state and "new notes" overlay, we use NUQS out of the box, eg https://tilda-geo.de/regionen/radinfra?map=14.8/52.4713/13.4426&config=pdqyyt.7h39.16g9vk&osmNotes=true&osmNote=14.8/52.4715/13.4411&v=2 Hope this helps someone improving their URL state! |
This comment has been hidden.
This comment has been hidden.
hi! |
|
+1 |
|
this works fine btw const createZodParser = <T extends z.ZodType>(schema: T) =>
createParser<z.infer<T>>({
parse: (input) => schema.parse(input),
serialize: (input) => schema.parse(input),
});define your schema like this const parseAsStringZod = createZodParser(z.string().nonempty());
const parseAsDateZod = createZodParser(z.date());
const searchParams = {
id: parseAsStringZod,
expires: parseAsDateZod,
signature: parseAsStringZod,
};and use it like this: export default async function Page({ searchParams }: PageProps) {
const { id, expires, signature } = await loadSearchParams(searchParams, {
strict: true, // for strict validation
}); |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Draft PR: #448.
There's been a few requests for using Zod to validate parsed search params, both on the server and on the client.
This makes a lot of sense since:
A proposition is to add a new method to the parsers builder API,
withValidation, that takes in a Zod schema.parsefunction:Since errors thrown in parsers result in
nullor the default value (if specified) being returned, this should work and would not actually introduce a peer dependency on Zod. It would accept any function that takes in anunknownargument and returns the parser output type. Technically we'd only call the validator if the query value has successfully been parsed / deserialized from a string, so T would also be a suitable input type for the validator.Why not use Zod directly in place of parsers ?
Zod can behave like the
parsemethod: take a string, hydrate it into a JS data type, and run some validation on top while doing so.However, it does not provide the counterpart serialization function to transform that JS data type back into a string, which is necessary on query state updates.
Also, it would require all Zod schema definitions to be rooted in
z.string().transform(s => whatever), which is not what existing application logic would define (schema definitions are likely to be reused or derived from domain-specific definitions).Caveats & Limitations
Validation needs to happen synchronously. This is because the parsing + validation is done in each hook's internal state initialisation, (getting the correct value from the URL on mount), which must return a value synchronously.
Questions
withValidationcompose validations or reset them? Eg:Ecosystem
While Zod is popular, an emerging standard schema has been developed and is being adopted by schema-consuming tools. Conforming to its specs would allow using Zod or any other standard-schema compliant validation library.
All reactions