T
TanStackβ€’3w ago
foreign-sapphire

Get type inference without using defaultValues

Hello, I am trying not to use defaultValues in my form and resetting the form when the deliveryNote prop changes because I want my form to be synchronized with my data sources, both the deliveryNote and nextAvailableDeliveryNoteIdData are updated in realtime.
10 Replies
foreign-sapphire
foreign-sapphireOPβ€’3w ago
The issue is that now, the onSubmit handler is not able to infer the value data type with my form fields:
interface FormDeliveryNoteProps {
mode: FormMode;
deliveryNote?: DeliveryNoteTableRowFragment;
}

const defaultDeliveryNoteFormData = {
deliveryDate: new Date(),
clientId: null,
deliveryMethodId: null,
paymentMethodId: null,
invoiceId: null,
};

export default function FormDeliveryNote({ mode, deliveryNote, layout }: FormDeliveryNoteProps) {
const {
data: nextAvailableDeliveryNoteIdData,
} = useLiveQuery({
...
});

const form = useForm({
validators: {
onMount: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
onChange: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
},
onSubmit: async ({ value: deliveryNoteFormData }) => {
try {
if (mode === FormMode.Create) {
await createDeliveryNoteMutation({ variables: { deliveryNote: deliveryNoteFormData as CreateDeliveryNote } });
} else if (mode === FormMode.Update) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { clientId, ...rest } = deliveryNoteFormData;
await updateDeliveryNoteMutation({ variables: { deliveryNote: rest as UpdateDeliveryNote } });
}
return { status: "success" }
...
},
});

useEffect(() => {
form.reset({
id:
mode === FormMode.Update || mode === FormMode.ReadOnly
? (deliveryNote?.id ?? null)
: (nextAvailableDeliveryNoteIdData?.nextAvailableDeliveryNoteId ?? null),
deliveryDate:
mode !== FormMode.Create
? deliveryNote?.deliveryDate
? new Date(deliveryNote.deliveryDate)
: null
: new Date(defaultDeliveryNoteFormData.deliveryDate),
...
});
}, [deliveryNote, nextAvailableDeliveryNoteIdData, mode]);
interface FormDeliveryNoteProps {
mode: FormMode;
deliveryNote?: DeliveryNoteTableRowFragment;
}

const defaultDeliveryNoteFormData = {
deliveryDate: new Date(),
clientId: null,
deliveryMethodId: null,
paymentMethodId: null,
invoiceId: null,
};

export default function FormDeliveryNote({ mode, deliveryNote, layout }: FormDeliveryNoteProps) {
const {
data: nextAvailableDeliveryNoteIdData,
} = useLiveQuery({
...
});

const form = useForm({
validators: {
onMount: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
onChange: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
},
onSubmit: async ({ value: deliveryNoteFormData }) => {
try {
if (mode === FormMode.Create) {
await createDeliveryNoteMutation({ variables: { deliveryNote: deliveryNoteFormData as CreateDeliveryNote } });
} else if (mode === FormMode.Update) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { clientId, ...rest } = deliveryNoteFormData;
await updateDeliveryNoteMutation({ variables: { deliveryNote: rest as UpdateDeliveryNote } });
}
return { status: "success" }
...
},
});

useEffect(() => {
form.reset({
id:
mode === FormMode.Update || mode === FormMode.ReadOnly
? (deliveryNote?.id ?? null)
: (nextAvailableDeliveryNoteIdData?.nextAvailableDeliveryNoteId ?? null),
deliveryDate:
mode !== FormMode.Create
? deliveryNote?.deliveryDate
? new Date(deliveryNote.deliveryDate)
: null
: new Date(defaultDeliveryNoteFormData.deliveryDate),
...
});
}, [deliveryNote, nextAvailableDeliveryNoteIdData, mode]);
Previously, I was using defaultValues like this:
interface FormDeliveryNoteProps {
mode: FormMode;
deliveryNote?: DeliveryNoteTableRowFragment;
layout?: React.ReactNode;
}

const defaultDeliveryNoteFormData = {
deliveryDate: new Date(),
clientId: null,
deliveryMethodId: null,
paymentMethodId: null,
invoiceId: null,
};

export default function FormDeliveryNote({ mode, deliveryNote, layout }: FormDeliveryNoteProps) {
const {
data: nextAvailableDeliveryNoteIdData,
...
} = useLiveQuery({
...
});

const form = useForm({
defaultValues: {
id:
mode === FormMode.Update || mode === FormMode.ReadOnly
? (deliveryNote?.id ?? null)
: (nextAvailableDeliveryNoteIdData?.nextAvailableDeliveryNoteId ?? null),
deliveryDate:
mode !== FormMode.Create
? deliveryNote?.deliveryDate
? new Date(deliveryNote.deliveryDate)
: null
: new Date(defaultDeliveryNoteFormData.deliveryDate),
clientId: mode !== FormMode.Create ? (deliveryNote?.client.id ?? null) : defaultDeliveryNoteFormData.clientId,
...
},
validators: {
onMount: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
onChange: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
},
onSubmit: async ({ value: deliveryNoteFormData }) => {
try {
if (mode === FormMode.Create) {
await createDeliveryNoteMutation({ variables: { deliveryNote: deliveryNoteFormData as CreateDeliveryNote } });
} else if (mode === FormMode.Update) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { clientId, ...rest } = deliveryNoteFormData;
await updateDeliveryNoteMutation({ variables: { deliveryNote: rest as UpdateDeliveryNote } });
return { status: "success" };
...
},
});
interface FormDeliveryNoteProps {
mode: FormMode;
deliveryNote?: DeliveryNoteTableRowFragment;
layout?: React.ReactNode;
}

const defaultDeliveryNoteFormData = {
deliveryDate: new Date(),
clientId: null,
deliveryMethodId: null,
paymentMethodId: null,
invoiceId: null,
};

export default function FormDeliveryNote({ mode, deliveryNote, layout }: FormDeliveryNoteProps) {
const {
data: nextAvailableDeliveryNoteIdData,
...
} = useLiveQuery({
...
});

const form = useForm({
defaultValues: {
id:
mode === FormMode.Update || mode === FormMode.ReadOnly
? (deliveryNote?.id ?? null)
: (nextAvailableDeliveryNoteIdData?.nextAvailableDeliveryNoteId ?? null),
deliveryDate:
mode !== FormMode.Create
? deliveryNote?.deliveryDate
? new Date(deliveryNote.deliveryDate)
: null
: new Date(defaultDeliveryNoteFormData.deliveryDate),
clientId: mode !== FormMode.Create ? (deliveryNote?.client.id ?? null) : defaultDeliveryNoteFormData.clientId,
...
},
validators: {
onMount: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
onChange: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
},
onSubmit: async ({ value: deliveryNoteFormData }) => {
try {
if (mode === FormMode.Create) {
await createDeliveryNoteMutation({ variables: { deliveryNote: deliveryNoteFormData as CreateDeliveryNote } });
} else if (mode === FormMode.Update) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { clientId, ...rest } = deliveryNoteFormData;
await updateDeliveryNoteMutation({ variables: { deliveryNote: rest as UpdateDeliveryNote } });
return { status: "success" };
...
},
});
In the later case, the onSubmit value has the type correctly inferred
xenial-black
xenial-blackβ€’3w ago
this seems overcomplicated. I think a key could be the better approach for this
xenial-black
xenial-blackβ€’3w ago
xenial-black
xenial-blackβ€’3w ago
see this example you're telling React that the component has a dependency on the value, so it will force a reload if it changes essentially resetting the form back to the default values it has - in this case defaultValues that are derived from the passed prop
foreign-sapphire
foreign-sapphireOPβ€’3w ago
Oh looks promising, thanks! I guess that using key is as efficient/unefficient as any other approach because react will rerender the whole form anyways, right?
xenial-black
xenial-blackβ€’3w ago
I'm not sure how performance could be hit by this, actually. But considering the workaround you listed above that's needed to get it working without remounting, a key with a comment explaining its purpose is way more readable
foreign-sapphire
foreign-sapphireOPβ€’3w ago
Perfect, thankss ❀️
xenial-black
xenial-blackβ€’3w ago
cool use of the read-only bit by the way. At my workplace, we’re still trying to figure out the best way to manage read-only permissions for forms and the like. This could be helpful
foreign-sapphire
foreign-sapphireOPβ€’3w ago
I am just experimenting with that, the main issue now is that I have a field which should be readonly and "display only", it should not be submitted with the other form values. I have to manually delete it (I am trying to use delete keyword or object unpacking, both suck) from the onSubmit value πŸ˜΅β€πŸ’« which is cumbersome. Also the validation schemas are a issue, I have had to put the field with a zod's .optional() in the update schema but it should not be part of the schema... Hmmm is there a way to make that the form validators validate only some fields? I otherwise I guess I could switch to validating each field instead........ πŸ˜΅β€πŸ’« oh wait I think I got it The goal: If form mode is readonly: value should only be shown If form mode is edit: value should be shown, but it should be readonly. The field should not be submitted If form mode is create: value should be editable by the user and submitted Component receives a prop with the field value for the readonly and edit modes:
export default function FormDeliveryNote({ mode, deliveryNote, layout }: FormDeliveryNoteProps) {
export default function FormDeliveryNote({ mode, deliveryNote, layout }: FormDeliveryNoteProps) {
In the form, we define it in defaultValues only in the FormMode.Create case, using a default value. Type casting is required in order to avoid having null as the only inferred type
const form = useForm({
defaultValues: {
...(mode === FormMode.Create && { clientId: defaultDeliveryNoteFormData.clientId as number | null }),
const form = useForm({
defaultValues: {
...(mode === FormMode.Create && { clientId: defaultDeliveryNoteFormData.clientId as number | null }),
The form field should use the form state.values variable in the create FormMode.Create case and the prop variable value in the other cases
<form.Subscribe
selector={(state) => [mode === FormMode.Create ? state.values.clientId : (deliveryNote?.client.id ?? null)]}
children={([activeClientId]) => ...}
/>
<form.Subscribe
selector={(state) => [mode === FormMode.Create ? state.values.clientId : (deliveryNote?.client.id ?? null)]}
children={([activeClientId]) => ...}
/>
The form validators work perfectly!
validators: {
onChange: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
},
validators: {
onChange: mode === FormMode.Create ? createDeliveryNoteSchema : updateDeliveryNoteSchema,
},
My zod schemas:
export const baseDeliveryNoteSchema = z.object({
id: idSchema,
...
});

export const createDeliveryNoteSchema = baseDeliveryNoteSchema.extend({
clientId: idSchema,
});

export const updateDeliveryNoteSchema = baseDeliveryNoteSchema;
export const baseDeliveryNoteSchema = z.object({
id: idSchema,
...
});

export const createDeliveryNoteSchema = baseDeliveryNoteSchema.extend({
clientId: idSchema,
});

export const updateDeliveryNoteSchema = baseDeliveryNoteSchema;
What do you think @Luca | LeCarbonator πŸ₯°
xenial-black
xenial-blackβ€’3w ago
well, as far as the avoiding submission goes, you can check for the mode in onSubmit and not perform it in that case as for the update vs create, this will do for small cases. In case you have a big form where there's lots of differences between create and update (for example, a version controlled form), then field groups can help out with that

Did you find this page helpful?