DEV Community

ömer koşar
ömer koşar

Posted on • Edited on

Stop Rebuilding Multi-Step Form Logic — Use rhf-stepper Instead

A headless, lightweight wrapper for react-hook-form that handles step state and per-step validation with zero UI opinions.


If you've ever built a multi-step form in React, you know the drill:

  • Track which step the user is on
  • Validate only the current step's fields before moving forward
  • Register/unregister fields as steps change
  • Wire up Back/Next/Submit navigation
  • Keep it all in sync with react-hook-form

You write this logic once. Then you write it again on the next project. And again. Each time it's slightly different, slightly buggy, and tightly coupled to whatever UI library you're using.

I got tired of it, so I built rhf-stepper.

What is rhf-stepper?

It's a headless logic layer on top of react-hook-form. It manages step state, field registration, and per-step validation — but it renders zero UI. No buttons, no step indicators, no CSS. You bring your own.

That means it works with MUI, Ant Design, Chakra UI, Tailwind, plain HTML — anything.

A Quick Look

import { useForm, FormProvider } from 'react-hook-form'
import { Stepper, Step, Controller, useStepper } from 'rhf-stepper'

function CheckoutForm() {
  const form = useForm()

  return (
    <FormProvider {...form}>
      <form onSubmit={form.handleSubmit((data) => console.log(data))}>
        <Stepper>
          {({ activeStep }) => (
            <>
              <Step>
                {activeStep === 0 && (
                  <Controller
                    name="email"
                    rules={{ required: 'Email is required' }}
                    render={({ field, fieldState }) => (
                      <div>
                        <input {...field} placeholder="Email" />
                        {fieldState.error && <span>{fieldState.error.message}</span>}
                      </div>
                    )}
                  />
                )}
              </Step>

              <Step>
                {activeStep === 1 && (
                  <Controller
                    name="address"
                    rules={{ required: 'Address is required' }}
                    render={({ field }) => <input {...field} placeholder="Address" />}
                  />
                )}
              </Step>

              <Navigation />
            </>
          )}
        </Stepper>
      </form>
    </FormProvider>
  )
}

function Navigation() {
  const { next, prev, isFirstStep, isLastStep } = useStepper()

  return (
    <div>
      {!isFirstStep && <button onClick={prev}>Back</button>}
      {isLastStep
        ? <button type="submit">Submit</button>
        : <button onClick={next}>Next</button>
      }
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

That's a fully working multi-step form with per-step validation. When you call next(), it only validates the fields registered in the current step. Users don't see errors for steps they haven't reached yet.

Why Headless?

Most multi-step form libraries ship with their own UI — step indicators, buttons, animations. That sounds nice until you need to match your app's design system. Then you're fighting CSS overrides or wrapping everything in custom components to hide the defaults.

rhf-stepper avoids this entirely. It gives you the logic primitives:

  • activeStep — which step the user is on
  • jumpTo — jump to any step
  • next() / prev() — navigate with validation
  • isFirstStep / isLastStep — for conditional UI
  • Controller — drop-in replacement that auto-registers fields to their step

You compose these into whatever UI you want. A MUI Stepper header? An Ant Design Anchor sidebar? A custom step indicator with plain divs? All work the same way.

Real-World Examples

I built three live demos to show this flexibility:

MUI Stepper — A checkout form using Material UI's Stepper component as the step indicator, with TextField for inputs.

Ant Design Anchor — A survey form using Ant Design's Anchor component as a sidebar navigator, where clicking an anchor item jumps to that step.

Custom (Zero Dependencies) — A contact form with a fully custom step indicator, inputs, and select — all built with plain HTML and inline styles. No UI library at all.

All three use the exact same rhf-stepper API. The only difference is the UI layer.

Installation

npm install rhf-stepper react-hook-form
Enter fullscreen mode Exit fullscreen mode

That's it. No additional dependencies required.

Links


If you're building multi-step forms with react-hook-form and want to stop reinventing the step logic, give rhf-stepper a try. Feedback and contributions are welcome.

Top comments (0)