Resetting a field (or a form) doesn't emit an event
Hello,
I have a form with a value I sync with my search param.
To make it sync on mount (search param -> form), I use onMount listener of the field.
To make it sync back later (form -> search param), I intend to use onChange for every field change.
But I figured out that clearing a field doesn't actually emit any event I can listen to. I ended up moving search param reset function to the place where reset actually happens, but I believe the "reset" event might be a thing to have.
My example (simplified):
const formSchema = z.object({
userId: z.string(),
// other values
});
type Form = z.infer<typeof formSchema>;
const Component = () => {
const [searchParamUserId, setSearchParamUserId] = useSearchParam("userId");
const defaultValues: Partial<Form> = {
userId: undefined,
};
const form = useAppForm({
defaultValues: defaultValues as Form,
validators: {
onMount: formSchema,
onChange: formSchema,
onSubmit: formSchema,
},
});
return (
<form.AppField
name="userId"
listeners={{
onMount: ({ fieldApi }) => {
if (userId) {
fieldApi.setValue(searchParamUserId);
}
},
onChange: ({ value }) => setSearchParamUserId(value),
}}
>
{(field) => (
<UsersSelect
selected={field.state.value}
onSelect={(nextUserId) => {
// Clicking on the same user should reset value to undefined
if (nextUserId === field.state.value) {
form.resetField("userId");
// Unfortunately, resetting field doesn't emit a listener event
setSearchParamUserId(undefined);
} else {
field.setValue(nextUserId);
}
}}
/>
)}
</form.AppField>
);
};
I can see use cases for a reset event! One question about the code though - is there a reason why you don't directly pass searchParamUserId to defaultValues instead of setting it on mount?
is there a reason why you don't directly pass searchParamUserId to defaultValues instead of setting it on mount?
Yes, because resetField sets it back to the defaultValue which might be defined.
I could've used resetField with a second argument, but the form type doesn't let userId be optional.
I'm not sure how to type form with two generics (for the final validated version and the default one), so I use a defaultValues as Form hack.
is there a reason why you don't directly pass searchParamUserId to defaultValues instead of setting it on mount?
Yes, because
resetFieldsets it back to thedefaultValuewhich might be defined. I could've usedresetFieldwith a second argument, but the form type doesn't letuserIdbe optional.I'm not sure how to type form with two generics (for the final validated version and the default one), so I use a
defaultValues as Formhack.
Zod (as with other Standard Schemas) implements a concept of input vs. output types. You want the input to be optional, but the output to be forced as string. There's some helper functions you can create for that, one of which I'll list below as example.
Since forms operate on the input type, zod schemas should be typed as z.input instead of z.infer (which is the output type).
I recommend to also read up on transforming data with Standard Schemas in TanStack Form.
Example of a nullable input helper function:
/**
* Modifies the provided schema to be nullable on input, but non-nullable on output.
*/
export function nullableInput<TSchema extends ZodTypeAny>(schema: TSchema) {
return schema.nullable().transform((value, ctx: RefinementCtx) => {
if (value === null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Selection is required'
});
return z.NEVER;
}
return value;
});
}
Usage
const formSchema = z.object({
userId: nullableInput(z.string()),
// other values
});
type Form = z.input<typeof formSchema>;
// = { userId: string | null, ... }
type FormOutput = z.output<typeof formSchema>;
// = { userId: string, ... }
I am also in need for a reset event to handle clearing some side effects I have that are based on form value changes. There are some convoluted ways I can implement what I need, but it would be way cleaner if there was a listeners.onReset I could hook into.
@dlindahl alright, makes sense to me I'll look into it.
I also need the listeners.onReset feature. Generally, for search forms, if a user wants to quickly reset a filled form, form.reset() is very useful. However, once the search form's values are reset to their initial values, we need to rerun the data query function after the reset. At this point, the onReset hook/callback becomes very convenient. Additionally, the execution of form.reset() seems to be asynchronous, and there is no way to immediately get the reset data right after calling it. This is one of the primary reasons why onReset is most needed. For now, I can only get it like this:
const [formValues, setFormValues] = useState(...)
const form = useForm(...)
const handleReset = () => {
form.reset();
setTimeout(() => { // To the user, this feels a bit like magic
setFormValues(form.state.values)
}, 0)
}
Note: the scenario for me is: for setFormValues, I only need two occasions to call it. One is onSubmit, and the other is onReset (which the library doesn't provide yet). Otherwise, onChange is actually a good choice, but sometimes we don't always want to get real-time updated data (even if it's debounced).