useReducer hook

How to use the useReducer hook to better manage the state of your component.


Description

The useReducer hook is an alternative approach to the useState hook, but uses the redux-like state management. If you like and you're used to the Redux state management system, you will surely like this hook.

Common use cases for this hook are:

  • Complex state management
  • Handling a stateful variable that is an object
  • Moving the state logic outside of the component

By using a reducer to handle all state's modifications, you will be sure that you can only perform specific actions on the state, and not everything you want (like you can actually do with the useState hook).

When the state is modified the component will be re-rendered.

Although the useReducer hook adds more control, more stability, and more predictability to the state, it also adds more complexity to your code. Try to use the useReducer hook only when the state is complex and you want another eventual developer (or the yourself of the future) to only perform specific actions on it.


Usage

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

The useReducer hook accepts two parameters: the reducer function and the initial state (usually an object). The reducer function is a function that has two arguments: the current state, and the action. The action is a simple object that usually have the "type" key (it's a convention, but you're free to use whatever you want), which is a string corresponding to the action you want to perform to modify the state. The reducer must return the new state or a portion of it.
The action is what you will pass as the first argument when you call the dispatch function.
Right now everything will probably sound confusing, but i'll guarantee that everything will be clearer once you see an example, so let's go straight to it.


Example: zoo population

In this example we will keep track of a zoo population using the useReducer hook.

import { useReducer, html, defineWompo } from 'wompo';

function reducer(state, action) {
  switch (action.type) {
    case 'add_lion': {
      return { lions: state.lions + 1 };
    }
    case 'add_zebra': {
      return { zebras: state.zebras + 1 };
    }
    case 'add_bear': {
      return { bears: state.bears + 1 };
    }
    default: {
      throw new Error('This action is not supported!');
    }
  }
}

const initialZooPopulation = {
  lions: 5,
  zebras: 10,
  bears: 2,
};

export default function Zoo() {
  const [zoo, dispatch] = useReducer(reducer, initialZooPopulation);

  const addLion = () => dispatch({ type: 'add_lion' });
  const addZebra = () => dispatch({ type: 'add_zebra' });
  const addBear = () => dispatch({ type: 'add_bear' });

  return html`
    <p>Lions: ${zoo.lions} <button @click=${addLion}>Add</button></p>
    <p>Zebras: ${zoo.zebras} <button @click=${addZebra}>Add</button></p>
    <p>Bears: ${zoo.bears} <button @click=${addBear}>Add</button></p>
  `;
}

Result:

Notice how in the reducer you only return a portion of the state, and not the whole updated state. Under the hood, Wompo will merge the returned value with the whole state. Of course, you can even return the whole state using the spread operator.

The possibility to return a portion of the state only applies to objects, not arrays nor primitive values.


Example: shopping cart

In this example we will use the useReducer hook to handle a shopping cart.

import { useReducer, html, defineWompo } from 'wompo';

function reducer(state, action) {
  switch (action.type) {
    case 'add_product': {
      return [...state, action.product];
    }
    case 'remove_product': {
      return state.filter((item) => item.id === action.product.id);
    }
    case 'increase_product_quantity': {
      return state.map((item) => {
        if (item.id === action.product.id) {
          return {
            ...item,
            quantity: item.quantity + 1,
          };
        }
        return item;
      });
    }
    case 'decrease_product_quantity': {
      return state.map((item) => {
        if (item.id === action.product.id) {
          return {
            ...item,
            // The minimum quantity for a product is 1
            quantity: item.quantity > 1 ? item.quantity - 1 : item.quantity,
          };
        }
        return item;
      });
    }
    default: {
      throw new Error('This action is not supported!');
    }
  }
}

export default function ShoppingCart() {
  const [items, dispatchCart] = useReducer(reducer, []);

  // This function accepts a simple [actionType] parameter, which is a string that can be:
  // "add_product" | "remove_product" | "increase_product_quantity" | "decrease_product_quantity"
  const executeAction = (actionType) => {
    dispatchCart({
      type: actionType,
      product: product,
    });
  };

  return html`...`;
}

defineWompo(ShoppingCart);

Using a reducer in this example will ensure that only specific operations can be performed on the state. For example, the developer will not be able to completely empty the cart, like you could have done using the useState hook. If you want to implement this functionality, you only have to modify the reducer and maybe add the action "empty_cart".

Notice how, using reducers, you "move" the state logic outside of the component. This allows to completely separate the state logic from the actual component.