Toggle with useReducer

We are now going to implement the toggle component using useReducer instead of useState.

We start from the Context type and initial value, the same as useState:

import { useReducer } from "react";

type Context = boolean;
const initialContext = false;

export default function UseReducer() {
  return (<></>);
}

With useReducer instead of directly calling setContext, we define events responsible for changing the context:

import { useReducer } from "react";

type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;

export default function UseReducer() {
  return (<></>);
}

Each event has a type property, which is used to identify the event.

In this example we have a single "toggle" event.

Outside of the component, we then define the reducer function responsible for updating the context.

reducer accepts the current context and an event, and returns the new context:

import { useReducer } from "react";

type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;

const reducer = (context: Context, event: Event): Context => {
  // 👇 On "toggle" event, invert the context `boolean`
  if (event.type === "toggle") {
    return !context;
  }
  return context;
};

export default function UseReducer() {
  return (<></>);
}

We connect the component to the reducer function by calling useReducer, passing the reducer function as the initial context:

import { useReducer } from "react";

type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;

const reducer = (context: Context, event: Event): Context => {
  if (event.type === "toggle") {
    return !context;
  }
  return context;
};

export default function UseReducer() {
  // 👇 Connect the component to the reducer function with the initial context
  const [context, dispatch] = useReducer(reducer, initialContext);
  return (<></>);
}

The UI code contains the same <input> as before with useState. Inside onChange instead of directly calling setContext we are now triggering the "toggle" event using send:

import { useReducer } from "react";

type Event = { type: "toggle" };
type Context = boolean;
const initialContext = false;

const reducer = (context: Context, event: Event): Context => {
  if (event.type === "toggle") {
    return !context;
  }
  return context;
};

export default function UseReducer() {
  const [context, dispatch] = useReducer(reducer, initialContext);
  return (
    <input
      type="checkbox"
      checked={context}
      onChange={() => dispatch({ type: "toggle" })}
    />
  );
}

Notice the separation of state logic outside of the component. The UI dispatches events to the reducer that exposes the context to the component
Notice the separation of state logic outside of the component. The UI dispatches events to the reducer that exposes the context to the component

That's all we need to do to create a toggle component with useReducer.

useReducer allows to separate the state logic from the component, which brings the following benefits:

  • The logic becomes reusable between components
  • We can test the logic independently of the component
  • We can understand the logic in isolation without looking at the component
  • All the logic is defined in the same place, which makes it easier to refactor, maintain, extend, and debug

The cost of this approach is that the code is now more verbose and understanding the full component requires to read both reducer and UI code.

The benefits of this refactoring are more apparent when the state logic is more complex.

Bare with me on this. For now, we are focusing more on understanding the differences between useState and useReducer with the objective of comparing them with XState.