Consuming state

Our application has the store connected to it, but the components are not consuming the state from the store. We can use Easy Peasy's useStoreState hook to do this.

If you aren't familiar with hooks, we highly recommend that you read React's official documentation on the subject. The documentation is really well written and will cover everything that you need to consider when using hooks based APIs.

Introducing the useStoreState hook

The useStoreState hook has the following signature.

useStoreState(State => MappedState)

The hook accepts a mapState function. The mapState function will be provided the state of your store and should return the slice of state required by your component.

Any time an update occurs on your store the mapState function will be executed, and if the newly mapped state does not equal the previously mapped state your component will be rendered with the new value.

Refactoring our components

We will now refactor each of the components in our application that are directly importing data from the src/data.js file to instead consume our store state.

To keep things concise we won't show the full source of the components, instead focusing on the changes that you will need to make within each of them. When you see a ... in the example code, it indicates that some of the source code has been omitted.

BasketCount

First up, the BasketCount component.

// src/components/basket-count.js

// ...
import { useStoreState } from "easy-peasy"; // 👈 import the hook

export default function BasketCount() {
  //       👇  map the state from store
  const basketCount = useStoreState(state => state.basket.productIds.length);
  // ...

Basket

Next, we will refactor the Basket component.

// src/components/basket.js

// ...
import { useStoreState } from "easy-peasy"; // 👈 import the hook

export default function Basket() {
  //       👇  map the state from store
  const basketProducts = useStoreState(state =>
    // take the product ids from our basket...
    state.basket.productIds.map(productId =>
      // and map them to products
      state.products.items.find(product => product.id === productId)
    )
  );
  // ...

The above mapping function looks fairly complicated, it is performing a fair amount of state deriving. Later on in the tutorial we introduce the computed API to help us with this case, providing us with performance optimisations and promoting re-use.

ProductList

Now we will refactor the ProductList component.

// src/components/product-list.js

// ...
import { useStoreState } from "easy-peasy"; // 👈 import the hook

export default function ProductList() {
  //       👇  map the state from store
  const products = useStoreState(state => state.products.items);
  // ...

Product

Finally, we will refactor the Product component.

// src/components/product.js

// ...
import { useStoreState } from "easy-peasy"; // 👈 import the hook

export default function Product({ id }) {
  //       👇  map the state from store
  const product = useStoreState(
    state => state.products.items.find(product => product.id === id)
  );
  // ...

This is another example of our mapState function performing some state deriving, however, in this case we are also using an incoming id prop within the state deriving process. Again, we shall later show how we can leverage the computed to help with this case.

A note on optimisation

Under the hood the useStoreState will execute its mapState function any time an update to your store's state occurs. It will then check the result of the newly mapped state against the previously mapped state using strict equality (===) checking.

If the newly mapped state is not equal to the previously mapped state (nextMappedState !== prevMappedState) your component will be rendered, receiving the new value. If the newly mapped state is equal to the previously mapped state (nextMappedState === prevMappedState) no render will occur.

With this in mind you should take care not to return a new Array or Object within your mapState function as these will break the equality check, forcing your component to render for any state update across your store.

An example.

function MyComponent() {
  const productNames = useStoreState(
    //                       👇 Array.map returns a new array instance!
    state => state.products.map(product => product.name)
  );
  // ...
}

Array.map returns a new array instance - therefore nextMappedState will never be equal to prevMappedState.

This performance pitfall is described within the useStoreState documentation along with recommendations on how you can avoid it. Later on in this tutorial we will cover some of these techniques so you need not go read the useStoreState documentation right now.

Whilst it is best to avoid the above, in many cases the performance hit will be negligible at best. Don't overstress about pre-optimisation - if you start to see performance issues you can later optimise your mapState functions. Again, we shall later introduce an API to help with the optimisation of these cases.

Review

Awesome sauce, our components are hooked up to our store's state! As amazing as that is, our application is essentially static right now, with no ability to update our store's state.

In the next section we'll look into how we can use actions in order to support updates.

You can view the progress of our application refactor here.