T
TanStackโ€ข4w ago
adverse-sapphire

How do you pass the form via the context api?

To preface: This is probably not a great idea but I'm trying to experiment with context and the idea of compound components. I'm trying to pass the form produced by useForm() to a context provider with the goal of using the hook in child components, here's what I have so far:
import { useForm } from '@tanstack/react-form'
import { createContext, useContext, type ReactNode } from 'react'
import z from 'zod'

export function createForm<Z extends z.ZodType>(
formSchema: Z,
defaultValues: z.input<Z>,
submitHandler: (value: z.input<Z>) => void,
) {
return useForm({
defaultValues: defaultValues,
validators: {
onSubmit: formSchema,
},
onSubmit: ({ value }) => {
submitHandler(value)
},
})
}

type FormInstance = ReturnType<typeof createForm>

const FormContext = createContext<FormInstance | null>(null)

const addExpenseFormSchema = z.object({
description: z
.string()
.min(1, 'You must provide a description')
.max(30, "Description can't exceed 30 characters"),
amount: z.number().min(1, 'You must provide the amount'),
category: z.string().refine((val) => val !== '', {
message: 'You must specify a category',
}),
})

export const FormProvider = ({ children }: { children: ReactNode }) => {
const form = createForm(
addExpenseFormSchema,
{amount: 0, category: "", description: ""},
console.log,
)
return <FormContext.Provider value={form}>{children}</FormContext.Provider>
}

export const useExpenseFormContext = () => {
const context = useContext(FormContext)
if (!context) {
throw new Error('useMyFormContext must be used within FormProvider')
}
return context
}
import { useForm } from '@tanstack/react-form'
import { createContext, useContext, type ReactNode } from 'react'
import z from 'zod'

export function createForm<Z extends z.ZodType>(
formSchema: Z,
defaultValues: z.input<Z>,
submitHandler: (value: z.input<Z>) => void,
) {
return useForm({
defaultValues: defaultValues,
validators: {
onSubmit: formSchema,
},
onSubmit: ({ value }) => {
submitHandler(value)
},
})
}

type FormInstance = ReturnType<typeof createForm>

const FormContext = createContext<FormInstance | null>(null)

const addExpenseFormSchema = z.object({
description: z
.string()
.min(1, 'You must provide a description')
.max(30, "Description can't exceed 30 characters"),
amount: z.number().min(1, 'You must provide the amount'),
category: z.string().refine((val) => val !== '', {
message: 'You must specify a category',
}),
})

export const FormProvider = ({ children }: { children: ReactNode }) => {
const form = createForm(
addExpenseFormSchema,
{amount: 0, category: "", description: ""},
console.log,
)
return <FormContext.Provider value={form}>{children}</FormContext.Provider>
}

export const useExpenseFormContext = () => {
const context = useContext(FormContext)
if (!context) {
throw new Error('useMyFormContext must be used within FormProvider')
}
return context
}
When passing the form into the provider as the value I get the following type error: https://pastebin.com/id9MekYP I'm guessing this is due to the use of generics, I suppose it's more to say this is a Zod/Typescript issue but I'm wondering if someone else has attempted something similar.
Pastebin
Type 'ReactFormExtendedApi<{ description: string; amount: number; c...
Pastebin.com is the number one paste tool since 2002. Pastebin is a website where you can store text online for a set period of time.
12 Replies
adverse-sapphire
adverse-sapphireOPโ€ข4w ago
I would love to create an abstraction for this component where you can just pass in a form schema and then through compound components add whatever element you wanted, something like
<AddExpenseForm.Root>
<AddExpenseForm.Header>
Some Header
</AddExpenseForm.Header>
<AddExpenseForm.Content>
<AddExpenseForm.FormProvider shema={someZodSchema} defaultValues={someTypeSafeDefaultValues} onSubmit={handleSubmit}>
<AddExpenseForm.Input name={someName} />
<AddExpenseForm.NumberInput name={someName} />
<AddExpenseForm.Select name={someName} options={someOptions} />
</AddExpenseForm.FormProvider>
</AddExpenseForm.Content>
</AddExpenseForm.Root>
<AddExpenseForm.Root>
<AddExpenseForm.Header>
Some Header
</AddExpenseForm.Header>
<AddExpenseForm.Content>
<AddExpenseForm.FormProvider shema={someZodSchema} defaultValues={someTypeSafeDefaultValues} onSubmit={handleSubmit}>
<AddExpenseForm.Input name={someName} />
<AddExpenseForm.NumberInput name={someName} />
<AddExpenseForm.Select name={someName} options={someOptions} />
</AddExpenseForm.FormProvider>
</AddExpenseForm.Content>
</AddExpenseForm.Root>
I would love for the schema to dictate what children can be present in the provider, and propogate that typesaftey through to the name where if you pass a name that isn't in the schema you get an error.
Goal being that if you want to add or remove fields you simply need to update your schema and add/remove components that are children of the provider
extended-salmon
extended-salmonโ€ข4w ago
I mean, this schema-oriented approach does sound interesting. I'll have to tinker with this myself, but the type error you see is because of a generics mismatch. Keep in mind there's libraries currently in development that try to be schema-first. Perhaps it's worth checking out if it's your ideal API. It's in v0, but here's the link: https://github.com/fabian-hiller/formisch
GitHub
GitHub - fabian-hiller/formisch: The modular and type-safe form lib...
The modular and type-safe form library for any framework - fabian-hiller/formisch
extended-salmon
extended-salmonโ€ข4w ago
would be great to have a stackblitz to go off of as well, since it's easier to share ideas that way
adverse-sapphire
adverse-sapphireOPโ€ข4w ago
ah neat I didn't know stackblitz was a thing, I'll try get an example going ๐Ÿ‘
adverse-sapphire
adverse-sapphireOPโ€ข4w ago
Sins_621
StackBlitz
Vitejs - Vite (duplicated) - StackBlitz
Next generation frontend tooling. It&#39;s fast!
extended-salmon
extended-salmonโ€ข4w ago
boots up! I can't check it out right now, but this will do for tinkering tomorrow, thanks!
adverse-sapphire
adverse-sapphireOPโ€ข4w ago
Amazing, appreciate your time ๐Ÿ™Œ
extended-salmon
extended-salmonโ€ข4w ago
I see what the approach is supposed to be with this structure, but we chose a different approach for Form composition for some specific reasons: Split concerns Given a basic structure of a form composition section:
<form.AppField name="name" validators={{}} listeners={{}} /* Logic related */>
{field => <field.TextInput label="Name" /> /* UI related */}
</form.AppField>
<form.AppField name="name" validators={{}} listeners={{}} /* Logic related */>
{field => <field.TextInput label="Name" /> /* UI related */}
</form.AppField>
We purposefully distinguish between what sections are the UI part and which sections are focused on the logic. Type safety With withForm, you ensure that the form that calls the component actually is compatible with it. If it's not, it will error. With moving to contexts-based API, you lose essentially all of it. Errors are now bound to runtime instead of compile time.
But field components and form components use context?
It's a part of the API that I'm personally not too fond of. Context is nice so that you can have composable fields, but the type safety is entrusted to you. There is a PR to address it, but the API isn't clear yet: https://github.com/TanStack/form/pull/1606
adverse-sapphire
adverse-sapphireOPโ€ข4w ago
I hear you, one of the initial issues I had with my approach was "What if the children don't satisfy the schema?" I don't think there's a useful way to indicate that to the developer, if that's what you were referring to in part with:
With moving to contexts-based API, you lose essentially all of it. Errors are now bound to runtime instead of compile time.
I had a read over the form composition docs: https://tanstack.com/form/v1/docs/framework/react/guides/form-composition and what I didn't like initially is I find it really difficult to understand how to decouple the UI components from the tanstack form itself but that's more likely a skill issue on my end
extended-salmon
extended-salmonโ€ข4w ago
Decouple them? Can you elaborate?
adverse-sapphire
adverse-sapphireOPโ€ข4w ago
Apologies, I'm having a hard time articulating here. An example I can give is that by using prebound components, if you wanted to extend the form, you'd have to add new components to that prebound list. If it was schema driven, I could add a field to my schema and add a new form input to the children of my provider. But listen I'm still super new to react and only have less than a year's programming experience under my belt and so my opinions both don't have much value at this stage and are ever changing ๐Ÿ˜… I'm going to thoroughly work through the examples on form composition using stackblitz (thanks for that suggestion) and then have I'll have a much more informed opinion after that. Really appreciate your time again, I'm not sure if 'questions' on discord can be closed but this one is 'resolved' as far as I'm concerned ๐Ÿ‘
extended-salmon
extended-salmonโ€ข4w ago
Discord Posts donโ€˜t have a resolved feature. Theyโ€˜ll get closed after inactivity

Did you find this page helpful?