Code Review Videos > JavaScript & TypeScript > React useReducer TypeScript Example

React useReducer TypeScript Example

Of all the hooks available in React, useReducer is easily my favourite. It’s nice to be able to solve simple problems with useState, but in component’s with even a smaller amount of complexity than can be solved with a boolean state value, I tend to favour useReducer.

Let’s dig in, see some examples, and find out why.

The examples I’m going to use are somewhat trivial only for the purposes of easy demonstration. The concepts learned are useful on larger, more complex components.

From useState to useReducer

The first example is a simple JavaScript / JSX counter component. For me, this would actually be preferable to keep as using the useState approach, but showing a way we are all familiar with, and how that transfers to useReducer has some value, I feel.

OK, here we go:

// src/Counter.jsx

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default Counter;Code language: JavaScript (javascript)

You could ‘upgrade’ this to a TypeScript component very easily:

// src/Counter.tsx

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState<number>(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default Counter;
Code language: TypeScript (typescript)

In this TypeScript version, we explicitly specify the type of the count state using useState<number>(0). This tells TypeScript that count is expected to be a number.

It really doesn’t make a huge amount of difference at this stage. But the types will come to help us as we progress.

In our current Counter component implementation we start with a number, 0 by default.

We can then click on a button to add one to the number, or remove one from the number.

I’ve added a bit of style using Tailwinds and Daisy UI, and without much effort we have something looking like this:

    <div>
      <p className="text-3xl mb-4">Count: {count}</p>
      <button onClick={increment} className="btn btn-neutral mr-2">
        Increment
      </button>
      <button onClick={decrement} className="btn btn-neutral">
        Decrement
      </button>
    </div>Code language: JavaScript (javascript)

Let’s pretend our Counter website suddenly got to the front page of Reddit, and usage is going to the moon 🚀🌚

Feature requests are flying in left and right. The biggest request, by far, is for a way to reset the counter.

Adding A ‘Reset’ Functionality

In this example, we’re managing the state of the count using useState. Now, let’s add a feature to reset the count back to the initial value. With useState, you can do it like this:

// src/Counter.jsx

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  const reset = () => {
    setCount(0);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

export default Counter;Code language: JavaScript (javascript)

With three different actions I’d already be starting to consider useReducer. These are simple transitions, but in reality our components often are more complex, even with a small handful of possible states. The more complex the state, the more complex the transitions between states, and once you have a bunch of components in play, remembering how everything works down to the nitty gritty can become challenging.

react counter component with reset button

Let’s see how this same component might look when using useReducer:

// src/Counter.jsx

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}

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

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
    </div>
  );
}

export default Counter;Code language: JavaScript (javascript)

I was going to do a line highlight here of everything that changed, but, well… everything has changed 🙂

Well, I say that, but only the internals have changed. The behaviour is actually fundamentally identical to what we had in the useState example.

Let’s break down the component and its key elements:

Initial State

   const initialState = { count: 0 };Code language: JavaScript (javascript)

Here, initialState defines the initial state of the component, which includes a single property count initialised to 0.

Reducer Function

   function reducer(state, action) {
     switch (action.type) {
       case 'INCREMENT':
         return { count: state.count + 1 };
       case 'DECREMENT':
         return { count: state.count - 1 };
       case 'RESET':
         return initialState;
       default:
         return state;
     }
   }Code language: JavaScript (javascript)

The reducer function is responsible for updating the state based on different actions that we define.

The reducer function takes the current state and an action object as parameters and returns a new state.

In this case, it handles three types of actions: 'INCREMENT', 'DECREMENT', and 'RESET'.

Depending on the action type, it updates the count property of the state accordingly.

useReducer Hook

   const [state, dispatch] = useReducer(reducer, initialState);Code language: JavaScript (javascript)

The useReducer hook is used to manage state in a more structured way. It takes two arguments: the reducer function and the initialState.

Much like useState, it returns an array with two elements:

  • state: This holds the current state, which is initially set to initialState.
  • dispatch: This is a function used to dispatch actions to the reducer, triggering state updates.

Rendered UI

Finally we have the actual output of our component. For simplicity I’ve kept the styles off this output.

   <div>
     <p>Count: {state.count}</p>
     <button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
     <button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
     <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
   </div>Code language: HTML, XML (xml)

We start by displaying the current value of count from the state object.

Then there are the three buttons: “Increment,” “Decrement,” and “Reset.”

When these buttons are clicked, they dispatch the corresponding action to the reducer function by calling dispatch({ type: 'OUR_ACTION_TYPE' }).

WebStorm actually gets a bit unhappy about some of this:

usereducer counter example webstorm

You can see a few bits are lightly underlined. No errors here, just warnings.

They all go away if we convert over to TypeScript:

// src/Counter.tsx

import React, { useReducer } from "react";

interface State {
  count: number;
}

type Action = { type: "INCREMENT" } | { type: "DECREMENT" } | { type: "RESET" };

const initialState: State = { count: 0 };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1 };
    case "DECREMENT":
      return { count: state.count - 1 };
    case "RESET":
      return initialState;
    default:
      return state;
  }
}

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

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "INCREMENT" })}>Increment</button>
      <button onClick={() => dispatch({ type: "DECREMENT" })}>Decrement</button>
      <button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
    </div>
  );
}

export default Counter;
Code language: TypeScript (typescript)

In the TypeScript version we need to tell TypeScript exactly what kind of things we expect and allow.

Initial State

On lines 5-7 we define a custom interface. It could be called anything, but as it will represent the components internal state, I went with the name State.

Our component handles one piece of state, and that is the count. The count is a number, much like we had back when we typed useState:

const [count, setCount] = useState<number>(0);Code language: HTML, XML (xml)

Action Type

The Action type is really where the most interesting part of this change occurs.

Again, Action is just a name. But it explains what concept we are wanting to describe.

Our component has three different things it can do. Three actions. Again, you don’t need to follow the convention given, but it’s really common to use a type key, and then a string property, typically in uppercase, to describe what is happening.

type Action = { type: "INCREMENT" } | { type: "DECREMENT" } | { type: "RESET" };Code language: JavaScript (javascript)

Here we have a union type – denoted by the pipe between the three different type definitions.

You could also write this as:

type Action = 
  | { type: "INCREMENT" } 
  | { type: "DECREMENT" } 
  | { type: "RESET" };Code language: JavaScript (javascript)

Which says that when we type something to be Action, it may be an object in any of them three different shapes.

const initialState: State = { count: 0 };Code language: JavaScript (javascript)

We defined the State interface, and now we tell TypeScript that our initialState object should have that type. In other words, initialState should be an object that has a count property with a numerical value.

Typed Reducer

And then we can add types to our reducer function:

function reducer(state: State, action: Action): State {Code language: JavaScript (javascript)

Which brings this all together.

The reducer function takes our two arguments, just like we covered earlier, only now they are typed.

The first argument is going to an object that has the type / shape of the State interface. So, again, some object that has a count property with a numerical value. It could be anything. Whatever properties you defined on your own component’s State.

And then an action.

That action can only be one of the three actions, in exactly those shapes, that we defined in the Action type. This works super well to give us some lovely intelli-sense / autocomplete in our IDE.

You can see this in action. When you use the dispatch function in your code, TypeScript is now clever enough to work out that the only viable and allowable arguments must match one of the Action shapes you defined:

typescript react component usereducer action typing

If you try to use a type that you didn’t specify in your Action type, or you add in some extra or incorrect property, TypeScript will throw out various warnings, and may stop you from compiling your code – depending on how strict your settings are in tsconfig.json.

TS2345: Argument of type { type: "INCREMENT"; someExtraKey: string; } is not assignable to parameter of type Action
Object literal may only specify known properties, and someExtraKey does not exist in type { type: "INCREMENT"; }Code language: CSS (css)

That might seem trivial on a small example component like this, but on larger projects, and with components you personally didn’t create, it can be a super helpful thing in my experience.

With this, WebStorm is really quite happy:

webstorm react component usereducer typescript example

Let’s look at one more example, which is a little more complex.

Shopping Basket Example

Whilst the Counter component shows everything involved in useReducer for pretty much every use case I’ve ever had, it still might not sell the idea to you because the example is trivial.

If we look at a slightly more complex component, the use of a reducer function – and how testable that function is – should hopefully sell this concept to you, if you aren’t already sold.

Using useState for Shopping Basket

We’ll start with useState and then refactor it to use useReducer to illustrate how and why useReducer can be a better approach in more complex scenarios:

// src/ShoppingBasket.jsx

import React, { useState } from "react";

const productVariations = [
  { id: 1, name: "Product 1", price: 10 },
  { id: 2, name: "Product 2", price: 5 },
  { id: 3, name: "Product 3", price: 15 },
  { id: 4, name: "Product 4", price: 8 },
  { id: 5, name: "Product 5", price: 12 },
];

function ShoppingBasket() {
  const [basket, setBasket] = useState([]);
  const [total, setTotal] = useState(0);

  const addToBasket = (product) => {
    setBasket([...basket, product]);
    setTotal(total + product.price);
  };

  const isProductInBasket = (productId) => {
    return basket.some((product) => product.id === productId);
  };

  const removeFromBasket = (product) => {
    const updatedBasket = basket.filter((item) => item.id !== product.id);
    setBasket(updatedBasket);
    setTotal(total - product.price);
  };

  const resetBasket = () => {
    setBasket([]);
    setTotal(0);
  };

  return (
    <div>
      <h2>Shopping Basket</h2>
      <ul>
        {basket.map((product) => (
          <li key={product.id}>
            {product.name} - £{product.price}
            <button
              onClick={() => removeFromBasket(product)}
            >
              Remove
            </button>
          </li>
        ))}
      </ul>
      <p>Total: £{total}</p>
        {productVariations.map((product) => (
          <button
            key={product.id}
            onClick={() => addToBasket(product)}
            disabled={isProductInBasket(product.id)}
          >
            Add {product.name}
          </button>
        ))}
        <button onClick={resetBasket}>
          Reset Basket
        </button>
      </div>
    </div>
  );
}

export default ShoppingBasket;Code language: JavaScript (javascript)

Here’s how that looks with a bit of style applied:

react shopping basket component example

And the behaviour is you can select each product, in any order, exactly once, before it is disabled / unavailable for selection. You can either remove it using the inline button, or clear / reset the entire basket:

react shopping basket component example with selections

Again, it is a trivialised component to a point. The aim is to show the more involved state updates.

Let’s break down each part of the state manipulation logic:

State Initialisation

   const [basket, setBasket] = useState([]);
   const [total, setTotal] = useState(0);Code language: JavaScript (javascript)

Two state variables, basket and total, are initialised using the useState hook.

basket represents the array of products in the shopping basket, and total represents the running total cost of the items currently in the basket.

Check if Product is in Basket

   const isProductInBasket = (productId) => {
     return basket.some((product) => product.id === productId);
   };Code language: JavaScript (javascript)

The isProductInBasket function takes a productId as an argument and checks if a product with that ID is already in the basket array. It uses the some method to determine if at least one item in the basket has a matching ID.

The use of some here is a left over from the original example I coded up. I wanted to be able to add multiple copies of the same product to the basket, but that got even more complicated.

Add to Basket

   const addToBasket = (product) => {
     if (!isProductInBasket(product.id)) {
       setBasket([...basket, product]);
       setTotal(total + product.price);
     }
   };Code language: JavaScript (javascript)

The addToBasket function takes a product as an argument and checks if the product is not already in the basket using isProductInBasket.

If the product is not in the basket, it updates the basket state by adding the new product and increases the total by the product’s price.

Two state updates here, and it’s critical that both are performed as an individual unit of behaviour. Hmm.

Remove from Basket

   const removeFromBasket = (product) => {
     const updatedBasket = basket.filter((item) => item.id !== product.id);
     setBasket(updatedBasket);
     setTotal(total - product.price);
   };Code language: JavaScript (javascript)

The removeFromBasket function also takes a product as an argument.

It creates a new array, updatedBasket, by filtering out the product with the matching ID.

The basket state is then updated with this new array, and the total is decreased by the price of the removed product.

Again we have two state updates happening in one function, both of which are absolutely critical to this component behaving properly.

Reset Basket

   const resetBasket = () => {
     setBasket([]);
     setTotal(0);
   };Code language: JavaScript (javascript)

The resetBasket function sets both the basket and total states to their initial values, effectively clearing the shopping basket.

We must remember to keep these two setters in sync with the default values used in the State Initialisation step. Yikes. What could go wrong?

Refactoring the useState Approach to TypeScript

Before we switch to useReducer, let’s take the existing plain JavaScript ShoppingBasket component and rewrite it using TypeScript.

// src/ShoppingBasket.tsx

import React, { useState } from "react";

interface Product {
  id: number;
  name: string;
  price: number;
}

const productVariations: Product[] = [
  { id: 1, name: "Product 1", price: 10 },
  { id: 2, name: "Product 2", price: 5 },
  { id: 3, name: "Product 3", price: 15 },
  { id: 4, name: "Product 4", price: 8 },
  { id: 5, name: "Product 5", price: 12 },
];

function ShoppingBasket() {
  const [basket, setBasket] = useState<Product[]>([]);
  const [total, setTotal] = useState<number>(0);

  const isProductInBasket = (productId: Product['id']): boolean => {
    return basket.some((product) => product.id === productId);
  };

  const addToBasket = (product: Product): void => {
    if (!isProductInBasket(product.id)) {
      setBasket([...basket, product]);
      setTotal(total + product.price);
    }
  };

  const removeFromBasket = (product: Product): void => {
    const updatedBasket = basket.filter((item) => item.id !== product.id);
    setBasket(updatedBasket);
    setTotal(total - product.price);
  };

  const resetBasket = (): void => {
    setBasket([]);
    setTotal(0);
  };

  return (
    <div>
      <h2>Shopping Basket</h2>
      <ul>
        {basket.map((product) => (
          <li key={product.id}>
            {product.name} - £{product.price}
            <button onClick={() => removeFromBasket(product)}>Remove</button>
          </li>
        ))}
      </ul>
      <p>Total: £{total}</p>
      {productVariations.map((product) => (
        <button
          key={product.id}
          onClick={() => addToBasket(product)}
          disabled={isProductInBasket(product.id)}
        >
          Add {product.name}
        </button>
      ))}
      <button onClick={resetBasket}>Reset Basket</button>
    </div>
  );
}

export default ShoppingBasket;Code language: TypeScript (typescript)

In this TypeScript version:

  • I added the Product interface to define the structure of a product.
  • The basket state is explicitly typed as an array of Product.
  • The total state is explicitly typed as a number.
  • Function parameters and return types are explicitly typed.
  • The isProductInBasket, addToBasket, removeFromBasket, and resetBasket functions have type annotations for better type safety.

There are a couple of things that I want to call out in slightly more detail.

Slightly Nicer Basket Reset

The first is that whilst we do have to still explicitly setBasket and setTotal back to default values, we do at least get some further assurances when using TypeScript:

We’re still responsible for setting the values, but TypeScript at least ensures they match the types we provided in the original calls to useState:

  const [basket, setBasket] = useState<Product[]>([]);
  const [total, setTotal] = useState<number>(0);Code language: TypeScript (typescript)

basket must be an empty array, or an array of things that look like Product.

And likewise, total can only be a number.

But … any number. We need not reset the total to 0 like we did in the original call. We still have to keep those two in sync. It sounds so trivial, but this is a legitimate source of bugs on larger projects.

Re-use Your Type Definitions: Indexed Access Types

This is one I see frequently out there in the real world:

const isProductInBasket = (productId: number): boolean => {
  return basket.some((product) => product.id === productId);
};

// versus

const isProductInBasket = (productId: Product['id']): boolean => {
  return basket.some((product) => product.id === productId);
};
Code language: TypeScript (typescript)

The two lines are functionally identical.

Our Product interface defines id as a number:

interface Product {
  id: number;
  name: string;
  price: number;
}Code language: CSS (css)

And either give us type safety:

typescript index access type

But we already defined the type of id on our Product interface. By defining the parameter of isProductInBasket as number, we have disconnected the two concepts.

Sure, they are both number … now. But what about in the future? What if we swap to a uuid or something? Well, now we need to update two places, or hope that our unit tests (which we do have, right?) catch the issue.

It sounds so trivial. However it, again, is a legitimate source of real world problems.

TypeScript calls this concept an Indexed Access Type. Why not use it?

Refactoring To useReducer

We’re almost there now.

I’m going to skip the JavaScript version of this component now, because in reality I haven’t made a plain JSX component in a very long time and have no intention of going back there when TypeScript exists.

We’ve covered the useState version of the ShoppingBasket. Now let’s refactor the code to retain the exact same behaviour, but make use of the useReducer hook.

// src/ShoppingBasket.tsx

import React, { useReducer } from "react";

interface Product {
  id: number;
  name: string;
  price: number;
}

interface State {
  basket: Product[];
  total: number;
}

type Action =
  | { type: "ADD_TO_BASKET"; product: Product }
  | { type: "REMOVE_FROM_BASKET"; product: Product }
  | { type: "RESET_BASKET" };

const initialState: State = {
  basket: [],
  total: 0,
};

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "ADD_TO_BASKET":
      return {
        basket: [...state.basket, action.product],
        total: state.total + action.product.price,
      };
    case "REMOVE_FROM_BASKET":
      const updatedBasket = state.basket.filter(
        (item) => item.id !== action.product.id,
      );
      return {
        basket: updatedBasket,
        total: state.total - action.product.price,
      };
    case "RESET_BASKET":
      return initialState;
    default:
      return state;
  }
};

const isProductInBasket = (
  basket: State["basket"],
  productId: Product["id"],
): boolean => {
  return basket.some((product) => product.id === productId);
};

const productVariations: Product[] = [
  { id: 1, name: "Product 1", price: 10 },
  { id: 2, name: "Product 2", price: 5 },
  { id: 3, name: "Product 3", price: 15 },
  { id: 4, name: "Product 4", price: 8 },
  { id: 5, name: "Product 5", price: 12 },
];

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

  return (
    <div>
      <h2>Shopping Basket</h2>
      <ul>
        {state.basket.map((product) => (
          <li key={product.id}>
            {product.name} - £{product.price}
            <button
              onClick={() => dispatch({ type: "REMOVE_FROM_BASKET", product })}
            >
              Remove
            </button>
          </li>
        ))}
      </ul>
      <p>Total: £{state.total}</p>
      <div>
        {productVariations.map((product) => (
          <button
            key={product.id}
            onClick={() => dispatch({ type: "ADD_TO_BASKET", product })}
            disabled={isProductInBasket(state.basket, product.id)}
          >
            Add {product.name}
          </button>
        ))}
        <button
          onClick={() => dispatch({ type: "RESET_BASKET" })}
        >
          Reset Basket
        </button>
      </div>
    </div>
  );
}

export default ShoppingBasket;
Code language: TypeScript (typescript)

There are two things that jump out at a glance:

  1. There is a lot of setup / pre-stuff
  2. The actual component rendering logic is now really quite slim

So yeah, we jumped up by about 30 lines.

Was it worth it?

I’d say so. And here’s why I think that.

Type Safety

We’ve explicitly described our component’s types:

interface Product {
  id: number;
  name: string;
  price: number;
}

interface State {
  basket: Product[];
  total: number;
}

type Action =
  | { type: "ADD_TO_BASKET"; product: Product }
  | { type: "REMOVE_FROM_BASKET"; product: Product }
  | { type: "RESET_BASKET" };Code language: TypeScript (typescript)

Throughout this post we’ve covered all of this information. The only addition is the inclusion of product in two of our Actions.

As I said earlier, our types are ours.

They represent our things. We can change them however we like, and however we need.

To add or remove a product to or from the basket, we need to say which Product we are working with. Now that information is captured in the specific Action. This means if we want to add a product to the basket, we must provide the product we are adding.

Makes a lot of sense.

Declare And ReUse Initial State

One of my big gripes with previous iterations of this component was the need to set and reset the initial state.

Now we define the components initial state only once:

const initialState: State = {
  basket: [],
  total: 0,
};Code language: TypeScript (typescript)

But we make use of it twice.

const initialState: State = {
  basket: [],
  total: 0,
};

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    // ...
    case "RESET_BASKET":
      return initialState;
    default:
      return state;
  }
};

// ...

function ShoppingBasket() {
  const [state, dispatch] = useReducer(reducer, initialState);
Code language: TypeScript (typescript)

It’s a small change, but it helps reduce bugs. Don’t repeat yourself, and all that.

Our State Is Highly Unit Testable

For me, being a test loving saddo, the biggest win is the fact that the components state is a highly testable pure function:

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "ADD_TO_BASKET":
      return {
        basket: [...state.basket, action.product],
        total: state.total + action.product.price,
      };
    case "REMOVE_FROM_BASKET":
      const updatedBasket = state.basket.filter(
        (item) => item.id !== action.product.id,
      );
      return {
        basket: updatedBasket,
        total: state.total - action.product.price,
      };
    case "RESET_BASKET":
      return initialState;
    default:
      return state;
  }
};Code language: JavaScript (javascript)

There’s something so satisfying about testing a pure function. Which is to say if the function is given the same arguments, it always returns the same result.

1 + 1 = 2

There’s no API calls firing off or reliance on some external state that may have changed leading to a different result.

Everything is self contained here. You could extract this logic out to a separate file, unit test it and import it back in. Confidence. I love it.

Monkey Jungle

A lot of this stuff is opinion.

Here’s another.

const isProductInBasket = (
  basket: State["basket"],
  productId: Product["id"],
): boolean => {
  return basket.some((product) => product.id === productId);
};Code language: JavaScript (javascript)

Does that need extracting?

I would say it makes it easier to unit test, so yes, extract it.

Another way to write this would be to have it inside the component function itself, and get access to state via closure:

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

  const isProductInBasket = (productId: number): boolean => {
    return state.basket.some((product) => product.id === productId);
  };Code language: TypeScript (typescript)

My argument against this is that firstly, you don’t need the full state object to figure out whether a product is in the basket.

And secondly, containing the isProductInBasket function inside the ShoppingBasket makes it really hard to test in isolation.

Extracting the function out, and only passing in the specific bit of state you need to get the outcome is preferable (again, imho):

<button
  key={product.id}
  onClick={() => dispatch({type: "ADD_TO_BASKET", product})}
  disabled={isProductInBasket(product.id)}
>

// versus

<button
  key={product.id}
  onClick={() => dispatch({ type: "ADD_TO_BASKET", product })}
  disabled={isProductInBasket(state.basket, product.id)}
>
Code language: HTML, XML (xml)

Why pass the entire state (the entire jungle) when you only need the basket array (the monkey)?

It just makes setting up your tests that much harder.

Wrap Up

That’s my take on why I prefer useReducer over useState in almost any component of real world complexity.

Of course your mileage my vary, these are only my opinions, and I’m always open to improving or changing, if a better way presents itself.

My experience has shown me that some developers tend to shy away from useReducer in favour of useState as they think the useReducer approach is more complex. I hope I have gone some way to convince you otherwise.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.