T
TanStack3w ago
genetic-orange

TypeScript: pass result of useForm as component props

Hi, I am trying to pass my form created by useForm via props to a child component but I am not sure how to properly type it in the TypeScript. Here is an example:
import { ReactNode } from "react";
import { useForm } from "@tanstack/react-form";

function Parent() {
const form = useForm({
defaultValues: {
emailAddress: "",
},
onSubmit: async ({ value }) => {
alert(value.emailAddress);
},
});

return (
// following line shows type error
<Child form={form}>
{(form) => (
<form.Field
name="emailAddress"
children={(field) => (
// render the form field
<></>
)}
/>
)}
</Child>
);
}

function Child<TForm extends ReturnType<typeof useForm>>(props: {
readonly form: TForm;
readonly children: (form: TForm) => ReactNode;
}) {
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
props.form.handleSubmit();
}}
>
{/* here goes some additional markup */}
{props.children(props.form)}
<props.form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit]) => (
<Button variant="contained" type="submit" disabled={!canSubmit}>
Submit
</Button>
)}
/>
</form>
);
}
import { ReactNode } from "react";
import { useForm } from "@tanstack/react-form";

function Parent() {
const form = useForm({
defaultValues: {
emailAddress: "",
},
onSubmit: async ({ value }) => {
alert(value.emailAddress);
},
});

return (
// following line shows type error
<Child form={form}>
{(form) => (
<form.Field
name="emailAddress"
children={(field) => (
// render the form field
<></>
)}
/>
)}
</Child>
);
}

function Child<TForm extends ReturnType<typeof useForm>>(props: {
readonly form: TForm;
readonly children: (form: TForm) => ReactNode;
}) {
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
props.form.handleSubmit();
}}
>
{/* here goes some additional markup */}
{props.children(props.form)}
<props.form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit]) => (
<Button variant="contained" type="submit" disabled={!canSubmit}>
Submit
</Button>
)}
/>
</form>
);
}
Type error shown:
Type 'ReactFormExtendedApi<{ emailAddress: string; }, FormValidateOrFn<{ emailAddress: string; }> | undefined, FormValidateOrFn<{ emailAddress: string; }> | undefined, ... 6 more ..., unknown>' is not assignable to type 'ReactFormExtendedApi<unknown, FormValidateOrFn<unknown> | undefined, FormValidateOrFn<unknown> | undefined, FormAsyncValidateOrFn<unknown> | undefined, ... 5 more ..., unknown>'.
10 Replies
genetic-orange
genetic-orangeOP3w ago
I understand that the problem is with Child type notation:
import { useForm } from "@tanstack/react-form";

function Child<TForm extends ReturnType<typeof useForm>>() {}
import { useForm } from "@tanstack/react-form";

function Child<TForm extends ReturnType<typeof useForm>>() {}
It should probably be something like this:
import { ReactFormExtendedApi } from "@tanstack/react-form";

function Child<TForm extends ReactFormExtendedApi>() {}
import { ReactFormExtendedApi } from "@tanstack/react-form";

function Child<TForm extends ReactFormExtendedApi>() {}
Unfortunately ReactFormExtendedApi type expects 10 type arguments.
Generic type 'ReactFormExtendedApi' requires 10 type argument(s). (ts 2314)
Has anyone tried this already? Is there a better/easier way? Or do I need to fill in all the 10 type arguments?
flat-fuchsia
flat-fuchsia2w ago
withForm allows you to safely type the form prop and you won‘t need to type the generics yourself
genetic-orange
genetic-orangeOP2w ago
Can you provide an example, please? Or point me to the documentation?
flat-fuchsia
flat-fuchsia2w ago
Sure, one sec
flat-fuchsia
flat-fuchsia2w ago
Form Composition | TanStack Form React Docs
A common criticism of TanStack Form is its verbosity out-of-the-box. While this can be useful for educational purposes helping enforce understanding our APIs it's not ideal in production use cases. As...
genetic-orange
genetic-orangeOP2w ago
Thanks for the link. I have read the documentation but I think that this time this is not something which can help me. I am trying to pass form into child component and I need kind of generic way of doing this as for instance my submit button is not rendered in parent component but I want it to be "hidden" as an implementation detail in my child component. I ended up with custom type as I didn't really need to do anything with the data - I only needed handleSubmit and Subscribe, this is what I ended up with:
import { ReactNode } from "react";

function Child(props: {
readonly form: {
handleSubmit: () => Promise<void>;
Subscribe: (props: {
selector: (state: {
canSubmit: boolean;
isSubmitting: boolean;
}) => [boolean, boolean];
children: (props: [boolean, boolean]) => ReactNode;
}) => ReactNode;
};
}) {
/* component here */
}
import { ReactNode } from "react";

function Child(props: {
readonly form: {
handleSubmit: () => Promise<void>;
Subscribe: (props: {
selector: (state: {
canSubmit: boolean;
isSubmitting: boolean;
}) => [boolean, boolean];
children: (props: [boolean, boolean]) => ReactNode;
}) => ReactNode;
};
}) {
/* component here */
}
Thanks anyway!
flat-fuchsia
flat-fuchsia2w ago
oh, this sounds like a classic example of a form component same docs section, where you can just use any form through context in that case, accessing its non-values state
genetic-orange
genetic-orangeOP2w ago
Thank you for your reply! I am not sure if I can imagine what you suggest. Would you be able to give me some code example? (does not to be proper code, just pseudo code). This is not urgent as I already have a workaround. If you would find few minutes of your time that would be great. I am using MUI 7 Dialog component (https://mui.com/material-ui/react-dialog/) Basically my structure looks like this:
function CustomDialog(props: {form}) {
return (
<Dialog>
<DialogTitle />
<form onSubmit={() => props.form.handleSubmit()}>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button>Cancel</Button>
<props.form.Subscribe />
</DialogActions>
</form>
</Dialog>
)
}
function CustomDialog(props: {form}) {
return (
<Dialog>
<DialogTitle />
<form onSubmit={() => props.form.handleSubmit()}>
<DialogContent>{children}</DialogContent>
<DialogActions>
<Button>Cancel</Button>
<props.form.Subscribe />
</DialogActions>
</form>
</Dialog>
)
}
The <form /> uses handleSubmit() from props.form and <DialogActions /> contains Subscribe for rendering submit button from props.form as well. Correct me if I am mistaken since I am not sure how exactly this works, but wouldn't I need custom hook for each form I would like to render within my <CustomDialog /> component?
React Dialog component - Material UI
Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks.
genetic-orange
genetic-orangeOP2w ago
In the meantime I am going to RTFM - I have a feeling that just quickly skimming through few sections is not sufficient. I am sorry for wasting your time as I believe that everything is already written down in the documentation.
flat-fuchsia
flat-fuchsia2w ago
with form composition, you can use useFormContext and get a general form API to work with. This doesn't include the form's values (as any form could use them), but it includes most meta state and sections you need.
// your-hook.tsx
export const { useFormContext, useFieldContext, formContext, fieldContext } = createFormHookContexts();

// submit-button.tsx
function SubmitButton(props: ButtonProps) {
const form = useFormContext();

// you can now use form.Subscribe, form state etc.
// render the button that you need
return <form.Subscribe selector={state => state.canSubmit}>
{canSubmit => <button {...props} disabled={props.disabled || !canSubmit}>Submit</button>}
</form.Subscribe>
}

// when creating your hook
const { useAppForm } = createFormHook({
fieldContext, formContext,
fieldComponents: {},
formComponents: { SubmitButton }
})

// Usage
const form = useAppForm({
// ... what you'd do for useForm
})

// Create context
return <form.AppForm>
<form.SubmitButton />
</form.AppForm>
// your-hook.tsx
export const { useFormContext, useFieldContext, formContext, fieldContext } = createFormHookContexts();

// submit-button.tsx
function SubmitButton(props: ButtonProps) {
const form = useFormContext();

// you can now use form.Subscribe, form state etc.
// render the button that you need
return <form.Subscribe selector={state => state.canSubmit}>
{canSubmit => <button {...props} disabled={props.disabled || !canSubmit}>Submit</button>}
</form.Subscribe>
}

// when creating your hook
const { useAppForm } = createFormHook({
fieldContext, formContext,
fieldComponents: {},
formComponents: { SubmitButton }
})

// Usage
const form = useAppForm({
// ... what you'd do for useForm
})

// Create context
return <form.AppForm>
<form.SubmitButton />
</form.AppForm>
since it's context, you can also nest them so you can have a form SubmitButton inside a form Dialog wrapper without problem what usually confuses people is that this context is stable, so make sure to use reactive state access like form.Subscribe

Did you find this page helpful?