T
TanStack2mo ago
causal-orange

Prop Drilling Necessary?

Hi folks. I'm looking into taking on a pretty large migration Tanstack Form from react-hook-form because we've realized how horrible of a library it is. Many footguns, implementation traps, etc, etc. react-hook-form embraces global hooks quite a bit, and to make the migration a bit simpler I was hoping to emulate that a bit in Tanstack form, but I've noticed the types are basically impossible to work with. It seems like this is by design, and the API forces you to prop drill the form prop anywhere you need it via the withFormhelper. Basically, I'd like to confirm this suspision? Is it unreasonable to use this library if I'm planning on storing the form in context?
6 Replies
foreign-sapphire
foreign-sapphire2mo ago
At the moment, prop drilling will be part of it, yes. It may change in the future (most likely by allowing form options to be passed to useFormContext), but it risks a lot of misuse to bypass something that either exists or should exist as a feature. If you‘re not sure if TSF is the right library to migrate to, check the philosophy section in the documentation. It should give some insight into what to expect from the library If you have a description / example snippet of your form structure, I could suggest some hints for it (or remember it for the prop drilling issue)
causal-orange
causal-orangeOP2mo ago
Thanks for the reply. We have a very large form that is split out into about 8 different pages, 50-60 fields. The form is save-able at any point, can be navigated through while dirty, advanced validations, the works. We basically hit every edge case that a form will run into, which is why we've had so many struggles with react-hook-form and why TSF looks so attractive to us. We've had multiple engineers on the team read through the philosophy, and we honestly just felt like we were having all of our current pain points validated. With that being said, we want to migrate, but the prop drilling is definintely a little annoying to have to deal with. I see the upside of it and the obvious type safety you get from it with the ability to avoid generics which is a big win. The structure looks something like the below, one form, with the form accessible at all levels in the hierarchy. Pretty much feels like every components needs to be wrapped with a withForm().
<Item>
<ItemBasicInformation />
<ItemAdvanced>
<Parent>
<Child>
<GrandChild />
</Child>
</Parent>
</ItemAdvanced>
<ItemOther />
<ItemSurvey />
<ItemExample />
</Item>
<Item>
<ItemBasicInformation />
<ItemAdvanced>
<Parent>
<Child>
<GrandChild />
</Child>
</Parent>
</ItemAdvanced>
<ItemOther />
<ItemSurvey />
<ItemExample />
</Item>
Another goal of mine to try to establish what our eventual APIs could look like with TSF, so we can start building towards that with RHF now to make the migration simpler. My initial stab at that was to isolate all usage to a single hook, like: useField() and use that for all of usage. But that would require the form to be in global state somehow, but it's very difficult to rebuild the type of form to have safe global state.
stormy-gold
stormy-gold2mo ago
Yeah, the challenge with avoiding prop drilling is that you end up losing a lot of the benefits of type safety, as you say. You COULD sidestep this by using AnyFormApi and such, but again loss of features and philosophy. That said, it's clear you're thinking through this systemically so if there's any ideas y'all have on how to balance this let us know. Can't promise we'll merge, but it might push us all to figure out the right answer
causal-orange
causal-orangeOP2mo ago
My idea is to utilize formOptions() to create a safer type which can then be exposed globally. It seems that formOptions() is already used for this purpose in conjunction with withForm to get proper typing as mentioned here:
// These values are only used for type-checking, and are not used at runtime // This allows you to ...formOpts from formOptions without needing to redeclare the options
We could follow that pattern: a generic type could be exported from TSF such as:
type FormApiFromFormOptions<T extends FormOptions> = FormApi<...>
// ^^^^
// Simplifiy generic setup of `FormApi` with helper
type FormApiFromFormOptions<T extends FormOptions> = FormApi<...>
// ^^^^
// Simplifiy generic setup of `FormApi` with helper
Usage could look like:
const formOpts = formOptions(...)
type MyForm = FormApiFromFormOptions<typeof formOpts>

// Now free to create a context, global state, etc with this *mostly* safe type
const MyFormContext = React.createContext<MyForm | null>(null);
const MyFormProvider = () => {
const form = useForm(formOpts); // may need typecast
return (
<MyFormContext.Provider value={form}>
{children}
</MyFormContext.Provider>
)
}

// Now you get typesafe global access, e.g.:
const useMyForm = () => useContext(MyFormContext);
const useMyEmailField = () => {
const form = useMyForm()
const field = useField(form, 'user.email'); // type safe!
}
const formOpts = formOptions(...)
type MyForm = FormApiFromFormOptions<typeof formOpts>

// Now free to create a context, global state, etc with this *mostly* safe type
const MyFormContext = React.createContext<MyForm | null>(null);
const MyFormProvider = () => {
const form = useForm(formOpts); // may need typecast
return (
<MyFormContext.Provider value={form}>
{children}
</MyFormContext.Provider>
)
}

// Now you get typesafe global access, e.g.:
const useMyForm = () => useContext(MyFormContext);
const useMyEmailField = () => {
const form = useMyForm()
const field = useField(form, 'user.email'); // type safe!
}
Theoretically, form is the same as type MyForm, but I think this requires forcing all useForm opts to be declared through formOptions(). I'm sure I missed some edge cases here, but this feels like there is potential here at least! LMK what y'all think!
sunny-green
sunny-green2mo ago
Apologies for the ping, but @crutchcorn – do you have any thoughts on this – Is this something that y'all would consider? I'd be happy to try to get the PR started if so where we can then collaborate. I'm the same person as @swushi btw. Didn't realize I was on my other account. Also silent tagging @Luca | LeCarbonator for thonks as he seems to have a lot of context on the subject based on the reading I've down around the channel.
foreign-sapphire
foreign-sapphire2mo ago
Interesting idea ... though I don't really see the benefits of this over directly passing formOpts into useFormContext. - MyFormProvider <-> form.AppForm - MyFormContext -> useFormContext(formOpts)

Did you find this page helpful?