Using actions to update state

Our application has the ability to consume state, but no way to update it.

In this section we will introduce the action API which will allow us to do exactly this.

Defining actions on our model

We are going to define two actions on our basketModel; one to add a product to our basket, and another to remove a product from our basket.

Let's go ahead and update our basketModel to include these two actions.

// src/model/basket-model.js

import { action } from 'easy-peasy'; // 👈 import

const basketModel = {
  productIds: [2],
  //  👇 define an action to add a product to basket
  addProduct: action((state, payload) => {
    state.productIds.push(payload);
  }),
  //  👇 define an action to remove a product from basket
  removeProduct: action((state, payload) => {
    state.productIds.splice(payload, 1);
  }),
};

export default basketModel;

Looking at the above you will note that we are defining actions directly against our model. Each definition includes a handler function which will be used to perform the state updates. The handler will receive the local state (in this case the basket model state) as well as any payload that was provided to the action when it was dispatched.

Within our handlers we are mutating the state directly to perform the update (🙈). Don't worry! We use the amazing immer library under the hood, which allows us to convert mutations into immutable updates against our store. It may seem magic, but it is so much more convenient and less error prone.

For example, look at the "immutable" approach at performing a state update:

addProduct: action((state, payload) => {
  return {
    ...state,
    productIds: [
      ...state.productIds,
      payload
    ]
  };
})

Woah! Far more verbose, and harder to grok! That being said, you can use this approach if you prefer. 😀

Next up let's learn how to dispatch our actions from our components.

Introducing the useStoreActions hook

We can access actions from our components via the useStoreActions hook, which has the following signature.

useStoreActions(Actions => MappedAction)

The hook accepts a mapActions function. The mapActions function will be provided the actions of your store and should return the action required by your component.

Dispatching actions from our components

We will now refactor our components to use the useStoreActions hook, allowing them to dispatch actions to update our state.

Product

First up, we will update the Product component so that its onAddToBasketClick callback function will dispatch an action to add the respective product to the basket.

// ...
import { useStoreActions, useStoreState } from 'easy-peasy';
//             👆 add the import

export default function Product({ id }) {
  //  map our action 👇
  const addProductToBasket = useStoreActions(
    actions => actions.basket.addProduct
  );

  // ...

  const onAddToBasketClick = useCallback(async () => {
    // ...
    addProductToBasket(product.id); // 👈 dispatch our action
    // ...
  }, [product]);

  return (
    <div>
      {/* ... */}
      <button onClick={onAddToBasketClick}>Add to basket</button>
    </div>
  );
}

Once you have updated your application accordingly you will be able to browse to a product and click the "Add to Basket" button. When doing so you should note that the basket count in the top right increases.

Basket

Next up, let's update the Basket component so that we can dispatch the action to remove a product from our basket.

// ...
import { useStoreActions, useStoreState } from 'easy-peasy';
//             👆 add the import

export default function Basket() {
  //  map our action 👇
  const removeProductFromBasket = useStoreActions(
    actions => actions.basket.removeProduct,
  );
  // ...

  return (
    <div>
      {/* ... */}
        {basketProducts.map((product, idx) => (
          {/* ... */}
          {/*                  dispatch the action 👇                      */}
          <button onClick={() => removeProductFromBasket(idx)}>Remove</button>
          {/* ... */}
        ))}
      {/* ... */}
    </div>
  );
}

After updating your application you should now be able to view the basket by clicking the link in the top right corner, and then remove a product from the basket via the "Remove" button next to each product.

Review

Things are starting to get interesting now. We can influence the state of our application via actions.

Earlier we noted that an emulated network call is being made when we add a product to our basket. Easy Peasy provides us with a mechanism to encapsulate side effects, such as API requests, within a thunk. We will learn this API in the next section.

You can view the progress of our application refactor here.