T
TanStack•5w ago
manual-pink

form composition with custom validation

I'm currently doing forms with custom (per field) validation with zod, which works kinda ok. typical pattern is:
<form.Field
name="name"
validators={{
onSubmit: ({ value }) => {
const parsed = createUserSchema
.pick({ name: true })
.safeParse({ name: value })

return parsed.error?.issues
},
}}
>
{(field) => (
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input
id="name"
value={field.state.value}
placeholder="Enter name..."
required
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
<FieldError errors={field.state.meta.errors} />
</Field>
)}
</form.Field>
<form.Field
name="name"
validators={{
onSubmit: ({ value }) => {
const parsed = createUserSchema
.pick({ name: true })
.safeParse({ name: value })

return parsed.error?.issues
},
}}
>
{(field) => (
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input
id="name"
value={field.state.value}
placeholder="Enter name..."
required
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
<FieldError errors={field.state.meta.errors} />
</Field>
)}
</form.Field>
and until now, while verbose, i was kinda ok with that. but new form i'm working on needs to be split into sub components. I have found the "choose your api" chart in https://tanstack.com/form/latest/docs/framework/react/guides/form-composition#api-usage-guidance and it seems that it's not possible to continue using useForm and custom validation functions with form composition, or am i mistaken?
59 Replies
dependent-tan
dependent-tan•5w ago
sort of. useAppForm is a supserset of useForm, so none of your existing code breaks when switching
manual-pink
manual-pinkOP•5w ago
@Luca | LeCarbonator thanks for your quick reply. does that mean i "only need" to switch useAppForm and then use the hooks to get form context in my children?
dependent-tan
dependent-tan•5w ago
So, say you have useForm and you need to migrate to useAppForm. The process would roughly be: * Create contexts, set up field/form components etc. * Note that field/form components are on the UI layer, not the logic layer. Read up on withFieldGroup if you need field logic to be part of it too * Change useForm to useAppForm * When accessing form components, wrap it in <form.AppForm> (the context provider) * Change fields that want field components from <form.Field> to <form.AppField> Once everything's set up, it would look sort of like this:
<form.AppForm>
<form.BlockNavigateAway /> {/* Logic like route blocking can be added on form level */}
<form.Form>
<form.AppField
name="name"
validators={{
onSubmit: ({ value, fieldApi }) => {
// There is `fieldApi.parseValueWithSchema()` if you want easier management
const parsed = createUserSchema
.pick({ name: true })
.safeParse({ name: value })
return parsed.error?.issues
},
}}
>
{(field) => (
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<field.StringInput required placeholder="Enter name..." />
<FieldError errors={field.state.meta.errors} />
</Field>
)}
</form.AppField>
</form.Form>
</form.AppForm>
<form.AppForm>
<form.BlockNavigateAway /> {/* Logic like route blocking can be added on form level */}
<form.Form>
<form.AppField
name="name"
validators={{
onSubmit: ({ value, fieldApi }) => {
// There is `fieldApi.parseValueWithSchema()` if you want easier management
const parsed = createUserSchema
.pick({ name: true })
.safeParse({ name: value })
return parsed.error?.issues
},
}}
>
{(field) => (
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<field.StringInput required placeholder="Enter name..." />
<FieldError errors={field.state.meta.errors} />
</Field>
)}
</form.AppField>
</form.Form>
</form.AppForm>
manual-pink
manual-pinkOP•5w ago
may i ask why the need to <field.StringInput /> ?
dependent-tan
dependent-tan•5w ago
StringInput would be a component accessing field context. <field. just gives you an easy autocomplete if you like if you instead want it to be through context only, you can pass it as child too
manual-pink
manual-pinkOP•5w ago
understood
dependent-tan
dependent-tan•5w ago
that also goes for form components
manual-pink
manual-pinkOP•5w ago
i was trying to create a wrapper which would take field as prop, but it seems quite challenging to type it properly. is there any type helper to do so?
dependent-tan
dependent-tan•5w ago
It depends on what you intend to do with the field passed as prop
manual-pink
manual-pinkOP•5w ago
probably making the whole <Field><FieldLabel> construction less verbose
dependent-tan
dependent-tan•5w ago
as mentioned, since it's through context, you can compose sections together. For example, you can add Input for granular control, but if you want a quick shortcut, you can wrap it in another field component called InputField which would do
<Field>
{label && <FieldLabel>{label}</FieldLabel>}
<Input />
</Field>
<Field>
{label && <FieldLabel>{label}</FieldLabel>}
<Input />
</Field>
internally
manual-pink
manual-pinkOP•5w ago
i'm not sure to follow (with the api) šŸ¤” i had in mind something from:
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input
id="name"
value={field.state.value}
placeholder="Enter name..."
required
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
<FieldError errors={field.state.meta.errors} />
</Field>
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input
id="name"
value={field.state.value}
placeholder="Enter name..."
required
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
<FieldError errors={field.state.meta.errors} />
</Field>
to
<FieldWrapper label="Name">
<Input
id={field.name}
value={field.state.value}
placeholder="Enter name..."
required
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
</FieldWrapper>
<FieldWrapper label="Name">
<Input
id={field.name}
value={field.state.value}
placeholder="Enter name..."
required
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
</FieldWrapper>
dependent-tan
dependent-tan•5w ago
Here's what I meant:
function InputComponent() {
const field = useFieldContext<string>();
// pass the boilerplate like onChange, onBlur etc.
return <Input
id={field.name}
// ...
/>
}

function InputFieldComponent() {
const field = useFieldContext();
// Do your field wrapper things here if needed
return <Field>
<InputComponent />
</Field>
}



// Later on
createFormHook({
// ...
fieldComponents: {
Input: InputComponent,
InputField: InputFieldComponent
}
})
function InputComponent() {
const field = useFieldContext<string>();
// pass the boilerplate like onChange, onBlur etc.
return <Input
id={field.name}
// ...
/>
}

function InputFieldComponent() {
const field = useFieldContext();
// Do your field wrapper things here if needed
return <Field>
<InputComponent />
</Field>
}



// Later on
createFormHook({
// ...
fieldComponents: {
Input: InputComponent,
InputField: InputFieldComponent
}
})
manual-pink
manual-pinkOP•5w ago
mmm i see.. i understand the construct but it'd be quite verbose to create a wrapper component for each of them (as they belong to shadcn component library) ; and i can't either edit their source because some inputs are used outside of form validation (for search bars etc) so i'd need to have a 1-1 mapping just for calling useFieldContext as a hook I could also do a HOC but i feel it's quite ugly :/ from {(field) => <Field><Input /></Field>} to {withFieldWrapper((field) => <Input />)}
dependent-tan
dependent-tan•5w ago
{(field) => <InputField /> also works also, most of the components don't actually rely on field context (like the label) the biggest ones are the input and the field errors
manual-pink
manual-pinkOP•5w ago
yes but for this i need to: - create one InputField component with hook inside - declare it as fieldComponents.InputField no? i'm still confused as why do i need to do that if the current above (without fieldComponents and with (field) => ReactNode) seems like working
dependent-tan
dependent-tan•5w ago
I'll ignore future features and PR drafts. Just looking at the current version of what fieldComponents and formComponents does it's mainly for intellisense. The form/field components are usually only used inside forms, so having an autocomplete to get the components you need makes it simpler. As far as runtime goes, you can import the component and use it directly too. Since it's passed with context, as long as you use <form.AppField>, you're good to go in cases like this, you don't even need the field anymore. This would do too:
<form.AppField // ...
>
{() => <InputComponent />}
</form.AppField>
<form.AppField // ...
>
{() => <InputComponent />}
</form.AppField>
manual-pink
manual-pinkOP•5w ago
yes, that i understand. because InputComponent has a useFieldContext() hook call inside it
dependent-tan
dependent-tan•5w ago
for your case, where you aren't sure if the input will be used for forms or for search bars, it could help distinguish it instead of checking that you imported the correct Field, you would know that everything under the <field. namespace is field-related and with lazy-loading, you avoid adding that overhead to calls that don't need it
manual-pink
manual-pinkOP•5w ago
i think i get it. in your reply i just noticed the nice comment about fieldApi.parseValueWithSchema but then i need to pass a single validator inside, right? for example createUserSchema.shape.name
dependent-tan
dependent-tan•5w ago
yeah it was initially added for combining callback validation with schema validation
manual-pink
manual-pinkOP•5w ago
there's no way for me to wrap this into a reusable function which would pick the shape[fieldApi.name] ?
dependent-tan
dependent-tan•5w ago
but I just noticed that you don't use the callback part of it apart from parsing If you are on zod version 3.24 or higher, you can pass it directly:
validators={{
onSubmit: yourSchema.shape.name
}}
validators={{
onSubmit: yourSchema.shape.name
}}
manual-pink
manual-pinkOP•5w ago
ok. it's one liner so i guess it's not that much
dependent-tan
dependent-tan•5w ago
well, if you have it on form level, it will pass errors to the fields so internally, it would do exactly that
manual-pink
manual-pinkOP•5w ago
yeah but my UX designer asked for "on blur individual field validation"
dependent-tan
dependent-tan•5w ago
the form validator approach would be "all errors, but don't show them until the field is blurred or the user tried submitting"
manual-pink
manual-pinkOP•5w ago
so i can't validate the whole form with a common schema and display errors, that's why i thought of using individual onSubmit fns
dependent-tan
dependent-tan•5w ago
which is usually a bit of boilerplate because of conditionally showing errors etc. Luckily, that is boilerplate that can be hidden inside field components
manual-pink
manual-pinkOP•5w ago
yeah i get that so i could get rid of those individual onSubmit even if i needed to see only errors one by one based on if they're dirty or not... mmm that's a lot to think about
dependent-tan
dependent-tan•5w ago
We use that approach at our workplace if you'd like an example
manual-pink
manual-pinkOP•5w ago
i wouldn't mind of course but I don't want to use too much of your time you've been tremendously helpful already
dependent-tan
dependent-tan•5w ago
feedback's always helpful, especially since we're discussing helper types and the like We used a common hook to have fields share common validation methods. It's not perfect yet (dependent fields sometimes don't show their errors until blurred or submitted), but it's quite nice as a start.
import { useStore } from '@tanstack/react-form';
import { isNotNil } from 'rambda';
import { useEffect, useRef } from 'react';
import { useFieldContext } from './FormContext';

function tryGetErrorMessage(errors: unknown[]): string | null {
// valid errors, sorted by priority:
// | string -> string
// | undefined | null -> null
// | { message: unknown } ->
// error.message
// | string -> string
// | null | undefined -> null
// | -> String(error.message)
// | -> String(error)

const error = errors.find(isNotNil);
if (!error) {
return null;
}

if (typeof error === 'string') {
return error;
}

if (typeof error === 'object' && 'message' in error) {
if (error.message === null || error.message === undefined) {
return null;
} else if (typeof error.message === 'string') {
return error.message;
} else {
return String(error.message);
}
}

return String(error);
}

export interface UseFieldMetaOptions {
/** The mode to use for validation.
* - 'onChange': Show the error if the field has been touched,
* or if the user attempted to submit without touching it.
* - 'onBlur': Show the error if the field is blurred,
* or if the user attempted to submit without blurring it.
* - 'always': Always show an error if there is one.
*/
mode: 'onChange' | 'onBlur' | 'always';
}

/**
* Extract reactive field metadata from a TanStack Field instance.
* Parses errors if there are any based on the Reward Early, Punish Late strategy.
*
* - If a value is invalid, as soon as the user fixes the input it should be green again (reward early).
* - If a value is valid, it shouldn't become red until the user finishes its input -- on blur or submission (punish late).
*/
export const useFieldMeta = (opts: UseFieldMetaOptions) => {
const field = useFieldContext();
const isValid = useStore(field.store, state => state.meta.isValid);
const isBlurred = useStore(field.store, state => state.meta.isBlurred);
const isDirty = useStore(field.store, state => state.meta.isDirty);
const errors = useStore(field.store, state => state.meta.errors);
const submissionAttempts = useStore(field.form.store, state => state.submissionAttempts);

const lastValidAttempt = useRef(submissionAttempts);
const showSubmitError = lastValidAttempt.current !== submissionAttempts;

useEffect(() => {
if (isValid && submissionAttempts > 0) {
// the user has corrected the field since the last submission
// requested changes.
lastValidAttempt.current = submissionAttempts;
}
if (isValid && isBlurred) {
// if the user changed the value so that it became valid,
// you can remove the blurred state.
field.setMeta(prev => ({ ...prev, isBlurred: false }));
}
}, [field, isValid, isBlurred, submissionAttempts]);

let showError: boolean;
switch (opts.mode) {
case 'always':
showError = !isValid;
break;
case 'onChange':
showError = !isValid && (isDirty || showSubmitError);
break;
case 'onBlur':
showError = !isValid && (isBlurred || showSubmitError);
break;
}

return {
isDirty,
isValid,
isBlurred,
submissionAttempts,
errorMessage: showError ? tryGetErrorMessage(errors) : null,
};
};
import { useStore } from '@tanstack/react-form';
import { isNotNil } from 'rambda';
import { useEffect, useRef } from 'react';
import { useFieldContext } from './FormContext';

function tryGetErrorMessage(errors: unknown[]): string | null {
// valid errors, sorted by priority:
// | string -> string
// | undefined | null -> null
// | { message: unknown } ->
// error.message
// | string -> string
// | null | undefined -> null
// | -> String(error.message)
// | -> String(error)

const error = errors.find(isNotNil);
if (!error) {
return null;
}

if (typeof error === 'string') {
return error;
}

if (typeof error === 'object' && 'message' in error) {
if (error.message === null || error.message === undefined) {
return null;
} else if (typeof error.message === 'string') {
return error.message;
} else {
return String(error.message);
}
}

return String(error);
}

export interface UseFieldMetaOptions {
/** The mode to use for validation.
* - 'onChange': Show the error if the field has been touched,
* or if the user attempted to submit without touching it.
* - 'onBlur': Show the error if the field is blurred,
* or if the user attempted to submit without blurring it.
* - 'always': Always show an error if there is one.
*/
mode: 'onChange' | 'onBlur' | 'always';
}

/**
* Extract reactive field metadata from a TanStack Field instance.
* Parses errors if there are any based on the Reward Early, Punish Late strategy.
*
* - If a value is invalid, as soon as the user fixes the input it should be green again (reward early).
* - If a value is valid, it shouldn't become red until the user finishes its input -- on blur or submission (punish late).
*/
export const useFieldMeta = (opts: UseFieldMetaOptions) => {
const field = useFieldContext();
const isValid = useStore(field.store, state => state.meta.isValid);
const isBlurred = useStore(field.store, state => state.meta.isBlurred);
const isDirty = useStore(field.store, state => state.meta.isDirty);
const errors = useStore(field.store, state => state.meta.errors);
const submissionAttempts = useStore(field.form.store, state => state.submissionAttempts);

const lastValidAttempt = useRef(submissionAttempts);
const showSubmitError = lastValidAttempt.current !== submissionAttempts;

useEffect(() => {
if (isValid && submissionAttempts > 0) {
// the user has corrected the field since the last submission
// requested changes.
lastValidAttempt.current = submissionAttempts;
}
if (isValid && isBlurred) {
// if the user changed the value so that it became valid,
// you can remove the blurred state.
field.setMeta(prev => ({ ...prev, isBlurred: false }));
}
}, [field, isValid, isBlurred, submissionAttempts]);

let showError: boolean;
switch (opts.mode) {
case 'always':
showError = !isValid;
break;
case 'onChange':
showError = !isValid && (isDirty || showSubmitError);
break;
case 'onBlur':
showError = !isValid && (isBlurred || showSubmitError);
break;
}

return {
isDirty,
isValid,
isBlurred,
submissionAttempts,
errorMessage: showError ? tryGetErrorMessage(errors) : null,
};
};
internally, we added a Input component, which is just the boilerplate wrapper:
<Form.Control
value={field.state.value}
name={field.name}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
isInvalid={isInvalid}
className={classNames({
'text-end': textAlign === 'end',
'text-start': textAlign === 'start',
'text-center': textAlign === 'center',
})}
{...controlProps}
/>
<Form.Control
value={field.state.value}
name={field.name}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
isInvalid={isInvalid}
className={classNames({
'text-end': textAlign === 'end',
'text-start': textAlign === 'start',
'text-center': textAlign === 'center',
})}
{...controlProps}
/>
Then, we added a wrapper that contains it since we oftentimes want everything in one spot.
function InputField(props: InputProps) {
const {
label,
inputBefore,
inputAfter,
classLabel,
textAlign,
showInvalid = 'all',
validateMode = 'onBlur',
...controlProps
} = props;
const field = useFieldContext<string>();
const id = useId();
const { errorMessage } = useFieldMeta({ mode: validateMode });

const isInvalid = showInvalid !== 'none' && errorMessage !== null;
const showFeedback = showInvalid === 'all';

return (
<Form.Group controlId={id}>
{label && <Form.Label className={classLabel}>{label}</Form.Label>}
<InputGroup hasValidation={showInvalid !== 'none'}>
{inputBefore}
<Input {...controlProps}/>
{inputAfter}
{showFeedback && <ErrorMessageFeedback errorMessage={errorMessage} />}
</InputGroup>
</Form.Group>
);
}
function InputField(props: InputProps) {
const {
label,
inputBefore,
inputAfter,
classLabel,
textAlign,
showInvalid = 'all',
validateMode = 'onBlur',
...controlProps
} = props;
const field = useFieldContext<string>();
const id = useId();
const { errorMessage } = useFieldMeta({ mode: validateMode });

const isInvalid = showInvalid !== 'none' && errorMessage !== null;
const showFeedback = showInvalid === 'all';

return (
<Form.Group controlId={id}>
{label && <Form.Label className={classLabel}>{label}</Form.Label>}
<InputGroup hasValidation={showInvalid !== 'none'}>
{inputBefore}
<Input {...controlProps}/>
{inputAfter}
{showFeedback && <ErrorMessageFeedback errorMessage={errorMessage} />}
</InputGroup>
</Form.Group>
);
}
it's not great ... inputBefore and inputAfter were later additions, when we realized that our form fields aren't as consistently structured as they should be I would have preferred granular control with common wrappers like the one above for the quick and easy route Example usage:
<Stack gap={3}>
<form.AppField name="name">
{field => <field.InputField label={pg.t('general.name')} />}
</form.AppField>
<form.AppField name="label">
{field => <field.InputField
label={pg.t('general.label')}
// The user should have immediate feedback for bad labels (wrong text, too long).
// error on blur feels bad to use
validateMode="onChange"
/>}
</form.AppField>
<form.SubmitButton className="ms-auto" text="save" icon="save" />
</Stack>
<Stack gap={3}>
<form.AppField name="name">
{field => <field.InputField label={pg.t('general.name')} />}
</form.AppField>
<form.AppField name="label">
{field => <field.InputField
label={pg.t('general.label')}
// The user should have immediate feedback for bad labels (wrong text, too long).
// error on blur feels bad to use
validateMode="onChange"
/>}
</form.AppField>
<form.SubmitButton className="ms-auto" text="save" icon="save" />
</Stack>
as far as logic goes, we always add in the form schema into onChange. But visually, the user never sees errors until submission It goes without saying I hope, but we don't use shadcn. You likely have to structure the UI a bit differently
manual-pink
manual-pinkOP•5w ago
mmm i see your InputField is what i was hoping for, but i didn't think of externalizing the logic as a hook. i would probably have done the 2 in 1 but use an open children slot with radix's Slottable to add props we're also lucky that i've been enforcing a single interface for all kind of inputs which is { value:T, onChange: (newVal: T) => void } so i was looking into leveraging that to pass magically value={ field.state.value} and onChange={field.handleChange} so that child
dependent-tan
dependent-tan•5w ago
there's some plans for that, but I've only seen snippets and haven't checked it out myself. The verbosity is part of being headless / framework agnostic. Allowing value and onChange to be spreadable would be nice, but it would break some UI libraries and React Native
manual-pink
manual-pinkOP•5w ago
yeah i understand talking to the world is another level x)
dependent-tan
dependent-tan•5w ago
I do have an urge to refactor the components once more. As mentioned above, we're adding lots of props to cover edge cases when we really just should have let ourselves be granular sometimes
manual-pink
manual-pinkOP•5w ago
i'm currently picking a small form and trying to refactor as suggested using a single schema validator in useForm({ validators{ onSubmit: createUserSchema } }) i'm running in a small quirk where one of the validator is expecting union of string | undefined but the defaultValues defines the field as string. should i just as string | undefined or is there a better way to handle this?
dependent-tan
dependent-tan•5w ago
it's an ongoing discussion, so I'll summarize a bit: * We're trying to prevent deadlocks where schemas like zod assign errors to fields that never exist * To do that, Standard Schema allows you to view the input type of schemas (zod: z.input<typeof schema>) * defaultValues should equal z.input<typeof schema> There's some problems with that, since your default values are often not the desired outcome. Addressing that is a whole other story, but for now, defaultValues should be assigned z.input<typeof schema> so that errors become clearer
const emptyForm: z.input<typeof schema> = {/* ... */}

function App() {
const form = useForm({ defaultValues: emptyForm, validators: { onChange: schema } })
}
const emptyForm: z.input<typeof schema> = {/* ... */}

function App() {
const form = useForm({ defaultValues: emptyForm, validators: { onChange: schema } })
}
manual-pink
manual-pinkOP•5w ago
i guess maybe passing the schema as generic to useForm rather than inferring from defaultValues would help :/ ?
dependent-tan
dependent-tan•5w ago
it would totally help, but TypeScript doesn't allow partial generics so you either specify none, or you specify all ... 15 at this point? Not sure what the current count is
manual-pink
manual-pinkOP•5w ago
hahahaha 12 :p
dependent-tan
dependent-tan•5w ago
i'm probably thinking of fields then
dependent-tan
dependent-tan•5w ago
Philosophy | TanStack Form Docs
Every well-established project should have a philosophy that guides its development. Without a core philosophy, development can languish in endless decision-making and have weaker APIs as a result. Th...
manual-pink
manual-pinkOP•5w ago
it's a very good design principle indeed šŸ™ not sure if that's a big deal but this looks ok to me:
useForm({
defaultValues: {} as z.infer<typeof createUserSchema>,
validators: { onSubmit: createUserSchema } },
})
useForm({
defaultValues: {} as z.infer<typeof createUserSchema>,
validators: { onSubmit: createUserSchema } },
})
dependent-tan
dependent-tan•5w ago
small nitpick, z.infer is the output of the schema, not the input
manual-pink
manual-pinkOP•5w ago
bonus point if i can:
function formData<T extends z.ZodObject>(schema: T) {
return {
defaultValues: {} as T,
validators: { onSubmit: schema },
}
}

useForm({
...formData(createUserSchema),
})
function formData<T extends z.ZodObject>(schema: T) {
return {
defaultValues: {} as T,
validators: { onSubmit: schema },
}
}

useForm({
...formData(createUserSchema),
})
oh nice catch thanks
dependent-tan
dependent-tan•5w ago
zod allows you to view the input type, yeah. It differs heavily between v3 and v4, so I'm not sure if the v3 code is helpful We use this helper:
/**
* Modifies the provided schema to be nullable on input, but non-nullable on output.
*/
export function nullableInput<TSchema extends ZodTypeAny>(schema: TSchema, opts?: NullableInputParams) {
return schema.nullable().transform((value, ctx: RefinementCtx) => {
if (value === null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
params: {
i18n: opts?.i18n ?? 'validation.selectionRequired',
},
});
return z.NEVER;
}
return value;
});
}
/**
* Modifies the provided schema to be nullable on input, but non-nullable on output.
*/
export function nullableInput<TSchema extends ZodTypeAny>(schema: TSchema, opts?: NullableInputParams) {
return schema.nullable().transform((value, ctx: RefinementCtx) => {
if (value === null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
params: {
i18n: opts?.i18n ?? 'validation.selectionRequired',
},
});
return z.NEVER;
}
return value;
});
}
manual-pink
manual-pinkOP•5w ago
ah so it's nullable but throws errors if so?
dependent-tan
dependent-tan•5w ago
in this case, yes z.input will be | null, but when calling schema.parse, it'll be non-nullable
manual-pink
manual-pinkOP•5w ago
aaah zod v4 apparently has z.output<T>
dependent-tan
dependent-tan•5w ago
probably slower than not making it nullable, but it's not the slowest part of our application so always had it
manual-pink
manual-pinkOP•5w ago
so this can be done, no?
export function formConfig<T extends z.ZodObject>(schema: T) {
return {
defaultValues: {} as z.output<T>,
validators: { onSubmit: schema },
}
}
export function formConfig<T extends z.ZodObject>(schema: T) {
return {
defaultValues: {} as z.output<T>,
validators: { onSubmit: schema },
}
}
dependent-tan
dependent-tan•5w ago
well, the defaultValues should be the input output considers transforms as well
manual-pink
manual-pinkOP•5w ago
ok i misunderstood what you said earlier šŸ™‚ indeed z.input<T> is best
dependent-tan
dependent-tan•5w ago
No description
dependent-tan
dependent-tan•5w ago
might as well be thorough
No description
manual-pink
manual-pinkOP•5w ago
@Luca | LeCarbonator thanks again for this discussion šŸ™
dependent-tan
dependent-tan•5w ago
no problem!

Did you find this page helpful?