DEV Community

Cover image for React Reducers
Ibrahim Shamma
Ibrahim Shamma

Posted on • Edited on

React Reducers

When it comes to reducers, you may often think that it is
related to the great Redux library, but it is originally comes from the React library itself.

We all have seen components that does one or two clicks, and they do many state changes, handling many state changes with useState can sometimes be tedious, we want to introduce another approach in handling multi-state changes inside your component.

I am not advising into stopping the usage of useState or the state management solutions, use this solution when the state is only the component's concern and we have many state changes ahead, for this case, you can consolidate all the state update logic outside your component in a single function, called a reducer.

A reducer is a way To reduce this complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component.

How to call it

import { useReducer } from "react";

const Componet = () => {

  const [state, dispatch] = useReducer<DispatcherType>(reducer, {
    tasks: [],
  });
....
}
Enter fullscreen mode Exit fullscreen mode

As you can see in the DispatcherType is function type that takes the currentState and the dispatched action as arguments, and returns the newState.

Dispatch:

The dispatch function by convention has type prop that infers the event name, it is advised that the event name is in snake_case and with with past tense

As can be seen here example:

dispatch({
  // specific to component
  type: 'what_happened',
  // other fields go here
});
Enter fullscreen mode Exit fullscreen mode

payload contains the event data that host the state changes, as recommended

creating a Project

TLDR Here is the running code example:

steps

We need to create a simple react app with the following steps:

npx create-react-app my-app --template typescript
cd ./my-app
Enter fullscreen mode Exit fullscreen mode

Once the app is created, we need to create component called task-list that will encapsulate the UI/Logic/Types and everything related to our simple app

mkdir ./src/components
mkdir ./src/components/task-list

touch ./src/components/task-list/lib.ts
touch ./src/components/task-list/types.ts
touch ./src/components/task-list/index.tsx
Enter fullscreen mode Exit fullscreen mode

We have in the component 3 distinct files:

  • types.ts: this will host the types of the component logic and state, this is great for newcomers to your code to understand what is actions in the ui and what is state being stored

  • lib.ts: this is where your presentational logic is stored

  • index.ts: Typical UI elements

This component setup is meant for simplicity, if you want a better setup for your working team, introduce to them FSD which in later posts I will dive into

Now let's fill the code:

// types.ts

export type Task = {
  id: string;
  name: string;
};

export type State = {
  tasks: Task[];
};

export type TaskAdded = {
  type: "task_added";
  payload: {
    task: string;
  };
};

export type TaskRemoved = {
  type: "task_removed";
  payload: {
    id: string;
  };
};

export type Action = TaskRemoved | TaskAdded;

export type Dispatch = (state: State, action: Action) => State;

Enter fullscreen mode Exit fullscreen mode
// lib.ts
import { Action, State } from "./types";
import { v4 } from "uuid";

export const reducer = (state: State, action: Action) => {
  console.log("state", state);
  console.log("action", action);

  switch (action.type) {
    case "task_added":
      return {
        tasks: [
          ...state.tasks,
          {
            id: v4(),
            name: action.payload.task,
          },
        ],
      };
    case "task_removed":
      return {
        tasks: state.tasks.filter((a) => a.id !== action.payload.id),
      };
    default:
      throw Error("Event Not Implemented");
  }
};
Enter fullscreen mode Exit fullscreen mode
// index.tsx

import { useState, useReducer } from "react";
import { reducer } from "./lib";
import { Dispatch } from "./types";

export const TaskList = () => {
  const [state, dispatch] = useReducer<Dispatch>(reducer, {
    tasks: [],
  });
  const { tasks } = state;
  const [currentInput, setCurrentInput] = useState<string>("");

  return (
    <div>
      <form>
        <input
          onChage={(e: any) => setCurrentInput(e.target.value)}
          placeholder="task"
          type="text"></input>
        <button
          type="button"
          onClick={() => {
            dispatch({
              type: "task_added",
              payload: {
                task: currentInput,
              },
            });
            setCurrentInput("");
          }}>
          submit
        </button>
      </form>
      {tasks.map((task, index) => (
        <div key={index}>
          <div>task {task.name}</div>
          <button
            type="button"
            onClick={() => {
              dispatch({
                type: "task_removed",
                payload: {
                  id: task.id,
                },
              });
            }}>
            delete
          </button>
        </div>
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)