T
TanStack4w ago
ambitious-aqua

Passing the form's type to a custom hook

I'm currently in the middle of refactoring from React Hook Form to TanStack Form, and I'm struggling to figure out how to pass the form's type to a custom hook. My goal is to abstract logic out of the component — things like inserting data into the form, setting errors, and so on. I've gone through the form composition docs, but they don't seem to cover how to handle this kind of abstraction using custom hooks. Am I missing something fundamental here? Any insights would be greatly appreciated! ❤️
12 Replies
metropolitan-bronze
metropolitan-bronze4w ago
Could you elaborate on what that abstract logic is? An example snippet (that ignores types) can help too!
ambitious-aqua
ambitious-aquaOP4w ago
Of course, sorry for not being clear enough @Luca | LeCarbonator . I have a form where one of the inputs fetches data from an endpoint, and the response is used to populate some of the other input fields in the form. I’d like to isolate the logic that inserts this data into the form fields into a separate function to keep the form component clean. Basically, what I want to achieve is a function like this:
// ✅ somePath/utils.ts
export const insertDataFromEndpointIntoInputs = (response: EndpointResponse, form: MyFormType) => {
form.setFieldValue("name", response.name);
};

//❌ forms/FormComponent.tsx
const FormComponent = () => {
const form = useForm({defaultValues: {name: string}});

// i dont want to define my insertDataFromEndpointIntoInputs function here
const insertDataFromEndpointIntoInputs = (...) => {...}
return <form>.....</form>
}
// ✅ somePath/utils.ts
export const insertDataFromEndpointIntoInputs = (response: EndpointResponse, form: MyFormType) => {
form.setFieldValue("name", response.name);
};

//❌ forms/FormComponent.tsx
const FormComponent = () => {
const form = useForm({defaultValues: {name: string}});

// i dont want to define my insertDataFromEndpointIntoInputs function here
const insertDataFromEndpointIntoInputs = (...) => {...}
return <form>.....</form>
}
Thank you. I really appreciate your input.
metropolitan-bronze
metropolitan-bronze4w ago
well, as it's specified in the philosophy section of the documentation, we're trying to avoid having to specify explicit types as much as possible. For situations like this, you have a couple of options: 1. It's purely external, but the field should receive data from it If the function fetches the data and populates fields with it, a good way to implement that is to use the updater function structure. It basically means that you have two ways of setting field values:
form.setFieldValue('name', 'someString') // directly
form.setFieldValue('name', previousValue => someCondition ? 'someString' : previousValue) // updater function
form.setFieldValue('name', 'someString') // directly
form.setFieldValue('name', previousValue => someCondition ? 'someString' : previousValue) // updater function
This allows you to extract fetching logic easily and pass it as the updater function to the form hook.
export const getDataFromEndpoint = (response: EndpointResponse): ResponseData => {
return response.data;
}

const FormComponent = () => {
const form = useForm({defaultValues: { data: { name: string } }});

form.setFieldValue('data', getDataFromEndpoint(response))
return <form>.....</form>
}
export const getDataFromEndpoint = (response: EndpointResponse): ResponseData => {
return response.data;
}

const FormComponent = () => {
const form = useForm({defaultValues: { data: { name: string } }});

form.setFieldValue('data', getDataFromEndpoint(response))
return <form>.....</form>
}
2. Field groups If the populated data is often associated with a specific (group of) fields in your form, then consider using a field group for it. Field groups allow you to preserve logic between fields, which can include setting such values. It may not be a suitable fit (since I don't know how your endpoint function works behind the scene), but it's worth looking into.
ambitious-aqua
ambitious-aquaOP4w ago
I read through the philosophy, but I now realize I didn’t fully grasp the “Generics are grim” section. There’s probably more I’m confused about, but what’s baffling to me right now is that I can’t seem to move the functionality into another file and work with the form instance there. It feels like I’m only able to work with the form in the file where its being instantiated (because it is not possible to specifiy the type of it). I think there's something fundamentally i don't understand yet. Thank you for taking the time to answer me on a sunday.
metropolitan-bronze
metropolitan-bronze4w ago
there's basic support for extracting functions to completely different files, but the problem is that giving full type support would require you to set 10 generic parameters ... not really user friendly. It's also bound to change in the future. Some helper types if it's only basic functionality you want extracted (not value or validator manipulation)
AnyFormApi
AnyFieldApi
AnyFormApi
AnyFieldApi
I also just noticed that you mentioned one input affects other inputs in the form. That actually sounds exactly like a field group use case. Here's a snippet of what that might look like
const CitySelector = withFieldGroup({
defaultValues: {
city: '',
postalCode: ''
},
render: function Render({ group }) {
const data = useQuery({/* ... */ }) // getting your city data
const city = useStore(group.store, state => state.values.city);

useEffect(() => {
// when city changes, update postal code if it exists
const code = data.postalCodes.get(city);
if (code) {
group.setFieldValue('postalCode', code)
}
}, [city])
}
})

// form component
const FormComponent = () => {
const form = useForm({defaultValues: { city: '', postalCode: '' }});
return <CitySelector form={form} fields={{
city: 'city',
postalCode: 'postalCode'
}} />
}
const CitySelector = withFieldGroup({
defaultValues: {
city: '',
postalCode: ''
},
render: function Render({ group }) {
const data = useQuery({/* ... */ }) // getting your city data
const city = useStore(group.store, state => state.values.city);

useEffect(() => {
// when city changes, update postal code if it exists
const code = data.postalCodes.get(city);
if (code) {
group.setFieldValue('postalCode', code)
}
}, [city])
}
})

// form component
const FormComponent = () => {
const form = useForm({defaultValues: { city: '', postalCode: '' }});
return <CitySelector form={form} fields={{
city: 'city',
postalCode: 'postalCode'
}} />
}
Notice how postalCode can be used as reference, it doesn't need to be generated as Field everytime. It allows to encapsulate logic between linked fields well
ambitious-aqua
ambitious-aquaOP4w ago
I will give it a shot. Appreciate it 🙂 Hey @Luca | LeCarbonator , After going through the documentation a couple more times and reviewing the snippet you sent, I feel like I’ve finally got a good handle on things. I do have one remaining question, though: since isolating the data insertion logic into a separate function doesn’t seem feasible without specifying 10 generics, how would you approach a case where the input order changes, or where a new field needs to be added between postalCode and city? (Assume the fieldGroup is reused across multiple forms.) Right now, the insertion logic feels tightly coupled to the UI structure—whereas a separate function wouldn’t need to care about the field order at all. Curious to hear how you'd approach this! 😄
metropolitan-bronze
metropolitan-bronze4w ago
I've actually had a similar problem at our workspace, where an interval field (start and end) could come in many variations such as horizontal or vertical stacking. My approach for it is to create more field groups that compose the common fields. With the postal code as example, your logic can be defined as
Fields: city, postalCode
Logic: city sets postalCode, postalCode needs no reference to city
Fields: city, postalCode
Logic: city sets postalCode, postalCode needs no reference to city
Therefore, you have two options of how to structure your field group. The first option restricts your UI more, where you pass postalCode and city, and handle both field UI renders as well as logic in the group. The second option is to make a group that accepts city and postalCodeReference, where it only renders city, but can set postalCode as field value. With this approach, you can consider fields to not just be a list of UI fields, but a list of dependencies of your logic. This dependency does not care about the structure of the form at all, as long as the fields are present
metropolitan-bronze
metropolitan-bronze4w ago
the shape outside remains the same, but internally, you can decide what part is UI and what part only needs the dependent logic
No description
ambitious-aqua
ambitious-aquaOP4w ago
Thanks! So the fields prop is basically just a pointer to where in the form state the setFieldValue operates? The example below is taken from your docs here:https://tanstack.com/form/latest/docs/framework/react/guides/form-composition#breaking-big-forms-into-smaller-pieces
<PasswordFields
form={form}
// You must specify where the fields can be found
fields="account_data"
title="Passwords"
/>
<PasswordFields
form={form}
// You must specify where the fields can be found
fields="account_data"
title="Passwords"
/>
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...
metropolitan-bronze
metropolitan-bronze4w ago
yeah! a pure string is just a shorthand way to do it
fields={{
a: 'foo.a',
b: 'foo.b'
}}

// is the same as

fields="foo"
fields={{
a: 'foo.a',
b: 'foo.b'
}}

// is the same as

fields="foo"
ambitious-aqua
ambitious-aquaOP4w ago
Awesome. Have a great day 😎
metropolitan-bronze
metropolitan-bronze4w ago
you too! good luck with the logic! also, if you have ideas for an API that completely decouples the logic, let me know! Perhaps a higher order function of some kind?

Did you find this page helpful?