My learning diary

useContext, useForm, and useFieldArray

I’m fortunate to have the opportunity to work with react-hook-form again. And work with it better. The following worked well for me:

// MultiStepForm.tsx

import { createContext, Dispatch, SetStateAction } from 'react'
import {
  SubmitHandler,
  useFieldArray,
  UseFieldArrayReturn,
  useForm,
  UseFormReturn,
} from 'react-hook-form'

interface SomeComplexInput {
    someKey: string
    someValue: string
}

interface MultiStepFormInput {
    formFieldA: string
    formFieldB: SomeComplexInput[]
}

interface OtherContext {
    // Other frontend-specific values e.g. active form step index
}

export const MultiStepFormContext = createContext(
  {} as UseFormReturn<MultiStepFormInput> &
    UseFieldArrayReturn<MultiStepFormInput, 'formFieldB', 'id'> &
    OtherContext,
)

export const MultiStepForm = () => {
    const form = useForm<MultiStepFormInput>({
        mode: 'all',
    })
    // Use fieldArray.replace and the like to change form fields on the fly
    const fieldArray = useFieldArray<MultiStepFormInput>({
        control: form.control,
        name: 'formFieldB',
    })

    const onSubmit: SubmitHandler<MultiStepFormInput> = (data: MultiStepFormInput) => {
        // Send data to your server
    }

    return (
        <form onSubmit={form.handleSubmit(onSubmit)}>
            <MultiStepFormContext.Provider
                value={{
                    ...form,
                    ...fieldArray,
                    // values from OtherContext
                }}
            >
                <SubsetOfFormFields />
            </MultiStepFormContext.Provider>
        </form>
    )
}
// SubsetOfFormFields.tsx

import { Input } from '@chakra-ui/react'

import { useContext } from 'react'

import { MultiStepFormContext } from './MultiStepForm'

export const SubsetOfFormFields = () => {
    const { fields, register } = useContext(MultiStepFormContext)

    // Inside map, field has access to both someKey and someValue
    return fields.map((field, index) => (
        <Input
          key={field.id}
          {...register(`formFieldB.${index}.someValue`)}
        />
    ))
}

Although SubsetOfFormFields is a direct child of MultiStepForm, I pass information via context. Any other child component won’t have to define extensive prop interfaces. They can call useContext.

This decision doesn’t suggest we should always opt for context. I use context when sharing information between stateful components. As for presentational ones, I pass information to them via props. I see SubsetOfFormFields as a stateful component. I see presentational components as pure functions that don’t mutate any state outside of itself.

For the sharp-eyed, you might’ve seen useFormContext and FormProvider in react-hook-form’s docs. Well, I forgot about it. If time and mood permit, I’ll investigate if I can add values from OtherContext into FormProvider. With type safety.

References:

Relevant posts