Software Engineering

My personal reference working with useReducer

`useReducer` helps us to manage mutiple states. It manages every state within a central place.To understand `useReducer`, I like to think about it as different people having different responsibilities.

KUZUE
August 17, 2026
9 min read
1 views

useReducer in React

This is my personal go to area in working with useReducer.

useReducer helps us to manage mutiple states. It manages every state within a central place.

I am going to demonstrate this using creating an account comprising email, password and name. It has severall component parts.


The Main Parts of useReducer

To understand useReducer, I like to think about it as different people having different responsibilities.

The State — The Vault/Books

The State is the current truth of your application.

For example:

ts
name = ""
email = ""
password = ""
loading = true

The state holds the information about what is currently happening in our application.


The Action

The Action is a structured note describing what you want to do.

It has a type, for example:

ts
"WITHDRAW_MONEY"

or:

ts
"UPDATE_EMAIL"

And it can have a payload, which is the actual information you want to send.

For example:

ts
"alice@gmail.com"

Dispatch

You can't talk directly to the reducer.

Instead, you talk to the reducer using:

ts
dispatch()

dispatch is responsible for sending the instruction.


The Reducer

The Reducer is the function which does all the changes.

The reducer receives the instruction from dispatch, looks at the current State, performs the calculation, and writes down the New State.

So the process looks like this:

text
User does something
       ↓
    dispatch()
       ↓
     Action
       ↓
    Reducer
       ↓
   New State

useReducer is structured such that duties are delegated.

For example, when a user types something, the dispatch captures the type of change that occurred, e.g.:

ts
"CHANGE_FIELD"

The payload, i.e. field and value, is then sent to the reducer.

The reducer now looks at the current state and performs the logic based on the type and returns the new state.


How Do We Know the Action Types?

To know the kinds of action type you will have in your application, imagine the user interacting with your form.

Write down every event they can trigger.

Step 1: Write Down the Events

| User interaction | Event | Action type | | ---------------------------------------- | -------------------- | ---------------- | | User types in an input | Something changed | CHANGE_FIELD | | User clicks a "Clear" or "Reset" button | Form reset | RESET_FORM | | User clicks "Submit" | Submission started | SUBMIT_START | | The database saves the data successfully | Submission succeeded | SUBMIT_SUCCESS | | The server responds with an error | Submission failed | SUBMIT_ERROR |

The idea is simple:

Think about what the user can do, then create action types around those events.


Step 2: Does the Reducer Need Extra Information?

Once you know the events, ask yourself:

"Does the clerk (reducer) need extra information to process this event?"

For RESET_FORM, the clerk doesn't need to know anything else.

ts
RESET_FORM

For CHANGE_FIELD, the clerk needs to know which input changed and what the new value is.

For SUBMIT_ERROR, the clerk needs to know what went wrong to show the user.

NB

It's better to group similar user interactions into a single action type, like:

ts
CHANGE_FIELD

and pass details in the payload instead of using:

ts
setName
setEmail
change_password

etc.

This keeps your reducer clean, scalable, and easy to maintain.


Step 1: Describe Our TypeScript Types

First we describe our TypeScript types.

What Does Our Form Data Look Like?

ts
interface FormFields {
    name: string;
    email: string;
    password: string;
}

This describes the information that belongs to our form.

We have:

text
name
email
password

What Does Our Form State Look Like?

ts
interface FormState {
    name: string;
    email: string;
    password: string;
    data: FormFields
}

What Actions Is the User Allowed to Request?

For actions we consider what the user can do with the form.

ts
type FormAction =
    | { type: "SUBMIT_START" }
    | { type: "SUBMIT_SUCCESS" }
    | { type: "SUBMIT_ERROR"; error: string }
    | { type: "CHANGE_FIELD"; field: keyof FormFields; value: string }
    | { type: "RESET_FORM" };

Here we have several possible actions:

text
SUBMIT_START
SUBMIT_SUCCESS
SUBMIT_ERROR
CHANGE_FIELD
RESET_FORM

Each action describes something that can happen in our application.


Step 2: Write the Reducer Function

The reducer is a pure function.

It takes the current state and the action, and returns a brand new state.

It never changes the existing state directly.

No mutating!

ts
function reducer(state: FormState, action: FormAction): FormState {
    switch (action.type) {
        case "SUBMIT_START":
            return {
                ...state,
                loading: true,
                error: null,
                success: false
            };

        case "SUBMIT_SUCCESS":
            return {
                ...state,
                loading: false,
                success: true,
                error: null,
                payload: {email: "", password: ""}
            };

        case "SUBMIT_ERROR":
            return {
                ...state,
                loading: false,
                error: action.error,
                success: false
            };

        case "CHANGE_FIELD":
            return {
                ...state,
                data: {
                    ...state.data,
                    [action.field]: action.value,
                },
            };

        case "RESET_FORM":
            return initialState;

        default:
            return state;
    }
}

The reducer receives two things:

ts
state

and:

ts
action

Then we use:

ts
switch (action.type)

to determine what should happen.

For example, when:

ts
action.type === "CHANGE_FIELD"

the reducer changes the appropriate field.


Step 3: Put It to Work in the Component

Now, we wire it up inside our React component using useReducer.

tsx
export default function Test() {
    const [open, setOpen] = useState(false);

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

    // Close modal on Escape key press
    useEffect(() => {
        if (!open) return;

        const handleKeyDown = (e: KeyboardEvent) => {
            if (e.key === "Escape") setOpen(false);
        };

        window.addEventListener("keydown", handleKeyDown);

        return () =>
            window.removeEventListener("keydown", handleKeyDown);
    }, [open]);

    // Handle Action Submit (Simulated API Call)
    const handleSubmit = async (e: FormEvent) => {
        e.preventDefault();

        dispatch({ type: "SUBMIT_START" });

        try {
            const res = await fetch("/api/sign-up", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify(state.data)
            });

            const g = await res.json();

            if (!res.ok) {
                throw new Error(g.message || "Network error");
            }

            dispatch({ type: "SUBMIT_SUCCESS" });

            dispatch({ type: "RESET_FORM" });

            setOpen(false); // Close modal on success
        } catch (err: any) {
            dispatch({
                type: "SUBMIT_ERROR",
                error:
                    err.message ||
                    "Something went wrong. Please try again."
            });
        }
    };

    return (
        <div className="mt-6 flex justify-center p-4">

            {/* Trigger Button */}
            <button
                onClick={() => setOpen(true)}
                className="flex items-center gap-2 px-5 py-2.5 bg-emerald-600 hover:bg-emerald-700 active:bg-emerald-800 text-white font-medium rounded-lg shadow-md hover:shadow-lg transition-all duration-200"
            >
                <ArrowBigDownDashIcon className="w-5 h-5" />

                <span>Add Sale</span>
            </button>

            {/* Modal Wrapper */}
            {open && (
                <div className="fixed inset-0 z-50 flex items-center justify-center p-4">

                    {/* Backdrop */}
                    <div
                        className="fixed inset-0 bg-slate-950/60 backdrop-blur-sm transition-opacity"
                        onClick={() => setOpen(false)}
                    />

                    {/* Modal Card */}
                    <div className="relative w-full max-w-md bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-2xl overflow-hidden transform transition-all p-6 flex flex-col gap-4 z-10 animate-in fade-in zoom-in-95 duration-200">

                        {/* Header */}
                        <div className="flex items-center justify-between pb-2 border-b border-slate-100 dark:border-slate-800">

                            <h2 className="text-lg font-semibold text-slate-900 dark:text-white">
                                Create a Product
                            </h2>

                            <button
                                onClick={() => setOpen(false)}
                                className="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-md p-1 transition-colors"
                                aria-label="Close modal"
                            >
                                <X className="w-5 h-5" />
                            </button>

                        </div>

                        {/* Form */}
                        <form
                            onSubmit={handleSubmit}
                            className="flex flex-col gap-4"
                        >

                            {state.error && (
                                <div className="text-sm p-3 bg-red-50 dark:bg-red-950/30 text-red-600 dark:text-red-400 rounded-lg border border-red-100 dark:border-red-900/50">
                                    {state.error}
                                </div>
                            )}

                            {/* Name field */}
                            <div className="flex flex-col gap-1.5">

                                <label
                                    htmlFor="name"
                                    className="text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400"
                                >
                                    Name
                                </label>

                                <input
                                    type="text"
                                    id="name"
                                    required
                                    value={state.data.name}
                                    onChange={(e) =>
                                        dispatch({
                                            type: "CHANGE_FIELD",
                                            field: "name",
                                            value: e.target.value
                                        })
                                    }
                                    placeholder="John Doe"
                                    className="w-full rounded-lg border border-slate-200 dark:border-slate-800 bg-transparent px-3 py-2 text-sm text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-all"
                                />

                            </div>

                            <button
                                type="submit"
                                disabled={state.loading}
                                className="w-full flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-sm transition-colors"
                            >

                                {state.loading ? (
                                    <>
                                        <Loader2 className="w-4 h-4 animate-spin" />

                                        <span>Creating...</span>
                                    </>
                                ) : (
                                    <span>Create Product</span>
                                )}

                            </button>

                        </form>

                    </div>
                </div>
            )}
        </div>
    );
}

Let's Understand What Is Happening

Let's slow down and look at the important part.

When the user types inside the input:

tsx
onChange={(e) =>
    dispatch({
        type: "CHANGE_FIELD",
        field: "name",
        value: e.target.value
    })
}

We are not directly changing the state.

We are sending a message to the reducer.

The message says:

text
Hey reducer!

The user changed a field.

The type is CHANGE_FIELD.

The field is name.

The new value is whatever the user typed.

The reducer receives this:

ts
{
    type: "CHANGE_FIELD",
    field: "name",
    value: e.target.value
}

Then the reducer looks at:

ts
action.type

and finds:

ts
case "CHANGE_FIELD":

It then performs the logic and returns the new state.


What Happens When We Submit?

When the user submits the form:

ts
dispatch({ type: "SUBMIT_START" });

The reducer receives:

ts
{
    type: "SUBMIT_START"
}

The reducer then changes the state to indicate that submission has started.

After the API request succeeds:

ts
dispatch({ type: "SUBMIT_SUCCESS" });

If something goes wrong:

ts
dispatch({
    type: "SUBMIT_ERROR",
    error: err.message
});

So our form has a flow:

text
User submits form
       ↓
 SUBMIT_START
       ↓
   API request
       ↓
 ┌─────┴─────┐
 ↓           ↓
Success     Error
 ↓           ↓
SUBMIT_     SUBMIT_
SUCCESS     ERROR

Questions I Would Ask My Students

Can we bypass the Reducer?

Can we change:

ts
state.name = "Bob"

directly?

Answer: No.

The state is read-only.

We must always dispatch an action.


What Is the Role of dispatch?

What is the role of:

ts
dispatch()

Answer:

It's the messenger.

It delivers our Action request slip to the Reducer.

Think about it like this:

text
You
 ↓
dispatch()
 ↓
Action request slip
 ↓
Reducer
 ↓
New State

Why Do We Write ...state Inside the Reducer?

Why do we write:

ts
...state

inside the reducer?

Answer:

React state is immutable.

ts
...state

copies the other fields, like email and password, so they don't get erased when we update the name.

For example:

ts
return {
    ...state,
    data: {
        ...state.data,
        [action.field]: action.value
    }
};

We are saying:

Keep everything that was already there, but update this particular field.


The Big Picture

When working with useReducer, remember the four important things:

text
STATE
  ↓
The current truth


ACTION
  ↓
What happened / what we want to do


DISPATCH
  ↓
The messenger


REDUCER
  ↓
The person responsible for changing the state

And the complete process is:

text
User interaction
       ↓
    dispatch()
       ↓
     Action
       ↓
    Reducer
       ↓
  New State
       ↓
 React updates UI

This is my personal go-to way of thinking about useReducer.

Comments (0)

No comments yet. Be the first to share your thoughts.