How to Replace useReducer with useActionState in React

The useReducer hook has been a trusted tool for managing complex state logic in React components, particularly when dealing with forms, multi-step processes, or tightly coupled state updates. But with the introduction of useActionState in React 18.2, there’s now a cleaner, more declarative alternative—especially when you’re working with forms.

While useReducer relies on dispatching actions and writing a reducer to handle state changes, useActionState eliminates the need for manual dispatching by allowing the form itself to drive state transitions. It’s purpose-built for handling form submissions and async state updates in a more integrated and streamlined way.

If you’re using useReducer to manage form state and want to migrate to a modern approach, this guide will walk you through how to effectively replace useReducer with useActionState.

Why Replace useReducer with useActionState

The primary reason is simplicity. With useActionState, you no longer need to handle event objects, manually call dispatch, or maintain an additional layer of abstraction between your form logic and state transitions. It brings you closer to a native HTML form workflow while still giving you control over the logic and state.

Additionally, useActionState is tightly integrated with React’s support for progressive enhancements like server actions and native form submissions, making it a more future-proof choice for handling form state.

Typical useReducer Setup in Forms

A common pattern using useReducer looks like this:

const initialState = {
  values: { name: '', email: '' },
  error: null,
  success: null,
};

function reducer(state, action) {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return {
        ...state,
        values: {
          ...state.values,
          [action.field]: action.value,
        },
      };
    case 'SUBMIT_SUCCESS':
      return { ...state, success: action.message, error: null };
    case 'SUBMIT_ERROR':
      return { ...state, error: action.message, success: null };
    default:
      return state;
  }
}

const [state, dispatch] = useReducer(reducer, initialState);

In the form:

<input
  name="name"
  value={state.values.name}
  onChange={(e) => dispatch({ type: 'UPDATE_FIELD', field: 'name', value: e.target.value })}
/>

<form onSubmit={(e) => {
  e.preventDefault();
  if (!state.values.name) {
    dispatch({ type: 'SUBMIT_ERROR', message: 'Name is required' });
    return;
  }
  dispatch({ type: 'SUBMIT_SUCCESS', message: 'Form submitted' });
}}>

This works but involves repetitive boilerplate and dispatch logic scattered throughout the component. It’s also less declarative and often harder to follow when forms grow in complexity.

Replacing useReducer with useActionState

Using useActionState, you can completely eliminate the need for onChange, onSubmit, and dispatch calls. Instead, the form submission itself becomes the action trigger. All validation and submission logic are handled inside a single async function.

Here’s how to replace the previous example:

'use client';

import { useActionState } from 'react';

const initialState = {
  error: null,
  success: null,
};

export default function ContactForm() {
  const [formState, formAction] = useActionState(async (prevState, formData) => {
    const name = formData.get('name');
    const email = formData.get('email');

    if (!name || typeof name !== 'string') {
      return { error: 'Name is required', success: null };
    }

    if (!email || typeof email !== 'string') {
      return { error: 'Email is required', success: null };
    }

    return { success: `Thanks, ${name}`, error: null };
  }, initialState);

  return (
    <form action={formAction}>
      <input name="name" />
      <input name="email" />
      <button type="submit">Submit</button>
      {formState.error && <p style={{ color: 'red' }}>{formState.error}</p>}
      {formState.success && <p style={{ color: 'green' }}>{formState.success}</p>}
    </form>
  );
}

This version removes the need for local value state, dispatch logic, and separate reducer function. The form is now self-contained and declarative. The formData object gives you access to submitted values directly without managing controlled inputs unless necessary.

Handling More Complex State Transitions

If your previous reducer handled more advanced transitions like loading, step changes, or different modes, you can encode the same logic in the useActionState reducer.

const initialState = {
  loading: false,
  error: null,
  success: null,
};

const [state, action] = useActionState(async (prevState, formData) => {
  const name = formData.get('name');
  if (!name) return { ...prevState, error: 'Missing name', success: null };

  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ loading: false, success: `Hello ${name}`, error: null });
    }, 1000);
  });
}, initialState);

You can also maintain previous state like step numbers, toggle states, or submission counts by including them in the reducer and returning updated state just like in useReducer.

Advantages of This Migration

Switching from useReducer to useActionState helps in the following ways:

  • Centralized form logic: all validation, side effects, and error handling in one place
  • Cleaner JSX: fewer props, no dispatchers
  • More declarative: the form’s action handles everything, reducing event handler clutter
  • Great integration with server actions in Next.js: server-side processing and file uploads become simpler
  • Better alignment with future React architecture: native form support, server actions, and async handling

When You Might Still Need useReducer

In some cases, useReducer is still a better fit:

  • When form inputs must be controlled and reflect immediate updates (e.g., dynamic fields, live previews)
  • When state changes are not tied to form submission (e.g., keyboard navigation, real-time interactivity)
  • For complex state machines where event-driven state transitions are easier to manage

But when your state changes are only driven by form submissions, and especially if you’re submitting to a server or doing async work, useActionState is usually a cleaner choice.

Final Thoughts

Replacing useReducer with useActionState in React is a step toward writing more declarative, maintainable, and future-ready forms. It reduces boilerplate, improves clarity, and aligns your component logic more closely with the browser’s native form behavior. By centralizing your async logic in a single reducer tied to the form itself, your code becomes easier to test, debug, and scale. If you’ve been using useReducer for form handling, consider migrating to useActionState—you might find your code gets smaller, smarter, and easier to work with.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top