#How do you pass the form via the context api?

15 messages ยท Page 1 of 1 (latest)

crystal ruin
#

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
}

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.

#

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>

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

arctic grove
# crystal ruin To preface: This is probably not a great idea but I'm trying to experiment with ...

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

The modular and type-safe form library for any framework - fabian-hiller/formisch

arctic grove
crystal ruin
#

ah neat I didn't know stackblitz was a thing, I'll try get an example going ๐Ÿ‘

arctic grove
crystal ruin
#

Amazing, appreciate your time ๐Ÿ™Œ

arctic grove
# crystal ruin https://stackblitz.com/edit/vitejs-vite-wcnmgjur?file=src%2FApp.tsx let me know ...

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>

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

crystal ruin
#

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.

crystal ruin
arctic grove
crystal ruin
#

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 ๐Ÿ‘