FrontendReactuseStateuseContextComponentFrontend

Building a Multi-Step Form in React with useReducer and Context

Multi-step forms require coordinated state across multiple screens. Learn how to architect them cleanly with useReducer, Context, and proper validation at each step.

Abdur Razzak

Abdur Razzak

Full-Stack Web Developer

May 26, 2026 9 min read

Multi-step forms are one of the most common and most challenging UI patterns in web development. They appear in user onboarding flows, checkout processes, survey builders, application forms, and account setup wizards. The challenge lies in coordinating state across multiple steps: preserving data entered in earlier steps when the user navigates forward and backward, validating each step independently before allowing progression, tracking overall progress and completion, and handling the final submission with all accumulated data. Using separate state variables for each step in each component leads to fragmented, difficult-to-maintain code. A centralized state management approach using useReducer for the form data and React Context to share that state across all step components produces a clean, testable architecture that scales gracefully as the number of steps and fields grows.

Designing the State Shape

The first step in building a multi-step form is designing the shape of the central state object. This state should contain all form field values organized by step, the index of the current active step, a record of which steps have been completed, and any step-level validation errors. Using a flat object for all fields works for small forms, but nesting fields by step name produces clearer code and makes it easier to validate one step at a time. The reducer function handles actions like setting a field value, moving to the next step, going back to a previous step, marking a step as complete, and submitting the form. Keeping all state transitions in the reducer rather than scattered across component event handlers makes the form logic easy to test in isolation, because pure reducer functions accept state and action and return new state without any side effects.

Setting Up the Context and Provider

Create a context object with createContext and a provider component that wraps the entire multi-step form. The provider initializes the form state with useReducer using the designed state shape as the initial value and exposes both the state and the dispatch function through the context value. All step components and navigation controls consume this context with useContext rather than receiving props, eliminating prop drilling through intermediate components that do not use the form data. Export a custom hook like useFormContext that calls useContext and throws a descriptive error if the context is undefined, which catches the common mistake of rendering a step component outside of the provider. The provider component also renders the active step component based on the current step index, centralizing the step routing logic in one place rather than distributing it across each step.

Step-Level Validation

Each step should validate its own fields before the user can proceed to the next step. Define a validation function for each step that accepts the current field values and returns an object mapping field names to error messages, or an empty object if all fields are valid. Call this validation function in the Next Step action handler before dispatching the step progression action. If validation fails, dispatch an action to set the validation errors in state and prevent step advancement. Display errors below each field using the error messages from state. Use a validation library like Zod or Yup to define field schemas declaratively rather than writing manual if-else validation logic, which becomes verbose for complex field combinations. Validate on blur for immediate feedback as the user tabs between fields, but only block progression on explicit submit of the step.

Navigation: Back, Next, and Step Indicators

The navigation controls live outside of individual step components and read from the shared context to know the current position and total number of steps. The Next button dispatches a validate-and-advance action. The Back button dispatches a step-back action without any validation, since the user should always be able to return to correct previously entered information. A step indicator component, often a horizontal progress bar with numbered or named steps, reads the currentStep and completedSteps values from context to display the appropriate active and completed states. Steps that have been completed should remain visually clickable to allow users to jump back and revise data, but jumping forward past uncompleted steps should be prevented by checking whether all intervening steps are marked complete before allowing the jump.

Handling the Final Submission

The final step submit handler collects all accumulated field values from the reducer state, performs a final full-form validation across all steps simultaneously rather than just the last step, and then calls the API. During the API call, dispatch an action to set a submitting flag that disables the submit button and shows a loading indicator. On success, dispatch a reset action to clear the form state and navigate to a confirmation page or summary view. On failure, dispatch an error action that sets a form-level error message and returns the user to the appropriate step if the error is field-specific. Always preserve all previously entered data on submission failure so users do not have to re-enter information they already provided. The centralized reducer architecture makes implementing this complete submission lifecycle considerably simpler than managing it across multiple component state variables.

Persisting Progress Across Page Refreshes

For multi-step forms in onboarding flows and lengthy applications, persisting the form state across accidental page refreshes significantly improves user experience. Integrate the useLocalStorage hook from earlier in the form provider to initialize state from a stored value when available. Subscribe to state changes and write the updated state to localStorage whenever the form data changes, excluding sensitive fields like passwords that should never be stored in browser storage. Add a clear function that removes the stored state and resets the reducer to its initial values, which you call on successful submission and on explicit user action. Display a banner when the user returns with saved progress, giving them the choice to continue where they left off or start fresh. This single enhancement dramatically reduces form abandonment for complex multi-step flows.

Share this article

All posts
#React#useState#useContext#Component#Frontend
Abdur Razzak — Full Stack Web Developer
⭐ Top Rated

Upwork Top Rated Developer

Work With a Developer Clients Trust