Practical React & Redux · Part 3

This is the third part of the React workshop series created by our team. Let's learn about how to use Redux to manage our application state.

This is the third part of the React workshop series created by our team. Let's learn about how to use Redux to manage our application state.

We have created a small React application with a bit of stateful components and our state is small at the moment. Today we are going to add Redux to manage the application state. This will make easier extending the state in future iterations of the project.

If you want to follow along, you can download the code here and can check out the part-2 tag and update the code as you read.

Introduction

We have a list of pets and each of them has a score. At the moment we can increase the score of each pet (but never decrease it because that would be unfair) and the actual value is stored in the component using the useState hook.

When our state grows it is better to use some kind of library to manage the state so each component could have access to it and also update it if necessary. That will make our application more reactive and extensible.

Managing application state: enters Redux

In the previous section we added some state to our components and that's totally fine but when the application grows on size and complexity it's hard to keep the state only in our components. Sometimes we need to share some piece of data across components and passing them just with props doesn't scale very well.

We are gonna learn about Redux and how to add a global application state to our pets collection!

Hello Redux!

Redux is just a small library so installing it is very straightforward. Since we are using React we can also install the react-redux package to make our life easier:

$ npm i redux react-redux

Our application state will live in something that is called store. The store will be accessible to all our components so it cannot just be added to some component's state. React has a cool feature called context that is used to share some data across components. We are not going to use the context directly because react-redux gives us a cool utility component called Provider. Let's see how to use it in our application:

// src/App.js
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'

import Dog from './dogs/components/dog/Dog'
import Cat from './cats/components/cat/Cat'

const App = () => {
  const store = createStore(() => {})

  return (
    <Provider store={store}>
      <Dog name="Boira" initialScore={13} />
      <Dog name="Black" initialScore={13} />
      <Cat name="Uhura" initialScore={13} />
    </Provider>
  )
}

export default App

As I mentioned before, we are going to use the Provider component to wrap our whole application (as an extra bonus we get rid of the weird <> thing 😜).

The Provider component has a prop called store. We are using the createStore method from Redux to create our store. We need to pass something that is called the "root reducer" to the createStore function. A reducer is just a function that receives our application current state and some action and returns the next state. Sounds familiar? No? Let me explain it 😬

If you recall the Javascriptreducer function does exactly that: it receives something (usually called accumulator) and some element. It uses that element and returns a new object (a new version of our accumulator). Let's see it in action:

const someNumbers = [1, 2, 3, 4, 5]
const sumReducer = (acc, element) => element + acc

console.log(`The sum is ${someNumbers.reduce(sumReducer)}`)

In the previous "yet another reducer example" we have something (an array of numbers) and we used the sumReducer function to transform that array to a single number (the sum of those numbers).

Still confused? Let's see how that works in Redux!

Reducers and actions: the core of Redux

The store will include our application state and you can think about it as a big plain object with some properties. The idea is we cannot modify that object directly, and we need a reducer function for that (remember, a function that receives the current state and an action and returns the next state). The key is the word action. An action is another plain object that includes a mandatory property called type and other optional properties. It looks something like this:

{
  type: "some action type",  // This is the only mandatory property
  payload: {                 // Optional properties are usually inside a payload
    someStuff: "whatever".   // property.
  }
}

How the reducer function receives this action? We can dispatch an action from anywhere! When an action is dispatched the reducer function receives it and computes the next state. We are going to see how to dispatch an action from our components later but let's continue with the root reducer function.

If you read the title of this section you may notice that I used the word "reducers" in plural. It is very common to have many reducers in an application and each reducer is in charge to handle one piece of state at a time. In other words, when an action is dispatched all reducers receives it and compute its piece of state. If some reducer doesn't know how to handle a specific action, it just returns the same state!

The Redux library already includes a method to combine reducers into a single one:

// src/App.js
import React from 'react'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'

import pets from './pets/store'
import Dog from './dogs/components/dog/Dog'
import Cat from './cats/components/cat/Cat'

const App = () => {
  const store = createStore(combineReducers({ pets }))

  return (
    <Provider store={store}>
      <Dog name="Boira" initialScore={13} />
      <Dog name="Black" initialScore={13} />
      <Cat name="Uhura" initialScore={13} />
    </Provider>
  )
}

export default App

This is going to explode because we haven't implemented the pets reducer but, as you can see, using the combineReducers function we can combine multiple reducers. We are using a plain object which each key is a piece of our global state and each value corrresponds to the reducer in charge of that piece of state.

Finally let's add a dummy reducer for our pets:

// src/pets/store/index.js
export const PETS_INITIAL_STATE = []

const pets = (state = PETS_INITIAL_STATE) => {
  return state
}

export default pets

The function pets is a reducer and it receives two params: the current state (or in other words, the value of the pets property in our application state) and the action that has been dispatched. Since the reducer is called once when loading our application I want to ensure that it has a correct initial value. Let's see how to use our new store!

Meet the container: a component that uses the store

We already have a collection of pets hardcoded in our application: two dogs and one cat. Let's move it to our application state now 👍

// src/pets/store/index.js
export const DOG = 'dog'
export const CAT = 'cat'

export const PETS_INITIAL_STATE = [
  { kind: DOG, name: 'Boira', initialScore: 13 },
  { kind: DOG, name: 'Black', initialScore: 13 },
  { kind: CAT, name: 'Uhura', initialScore: 13 },
]

const pets = (state = PETS_INITIAL_STATE, action) => {
  return state
}

export default pets

I added a simple array with some plain objects. I also added a kind property that we are going to use it later to decide which component to render, nothing really fancy 😎.

We can use this data in our application now! Wait... how do we read the application state? 🤦‍♂️. There is a React hook for that! The react-redux package includes a function that is called useSelector that "selects" a piece of our application state.

A common pattern when using a store is to make a difference between components that has access to the store and components that hasn't. If a component has access to the store it is called a container. Let's create our first container in our pets module (notice that the component is inside a containers folder):

// src/pets/containers/pets/Pets.js
import React from 'react'

import Dog from '../../../dogs/components/dog/Dog'
import Cat from '../../../cats/components/cat/Cat'

const Pets = () => {
  return (
    <>
      <Dog name="Boira" initialScore={13} />
      <Dog name="Black" initialScore={13} />
      <Cat name="Uhura" initialScore={13} />
    </>
  )
}

export default Pets

We have moved our pets from our App.js component (and our archenemy <> has returned 🤨) and leave it like this:

// src/App.js
import React from 'react'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'

import pets from './pets/store'
import Pets from './pets/containers/pets/Pets'

const App = () => {
  const store = createStore(combineReducers({ pets }))

  return (
    <Provider store={store}>
      <Pets />
    </Provider>
  )
}

export default App

Please, make sure your application is still working after this refactor! Let's hook (pun intended) our store to the new container!

// src/pets/containers/pets/Pets.js
import React from 'react'
import { useSelector } from 'react-redux'

import { DOG } from '../../store'
import Dog from '../../../dogs/components/dog/Dog'
import Cat from '../../../cats/components/cat/Cat'

const Pets = () => {
  const pets = useSelector((state) => state.pets)

  return (
    <>
      {pets.map(({ kind, name, initialScore }) =>
        kind === DOG ? (
          <Dog key={name} name={name} initialScore={initialScore} />
        ) : (
          <Cat key={name} name={name} initialScore={initialScore} />
        )
      )}
    </>
  )
}

export default Pets

OMG 😱! If your face looks like this... that's normal! There are tons of new stuff to digest on the previous snippet but after understanding it you are gonna level up a lot.

First we used the useSelector hook from react-redux. It uses a function that receives our application state so we can "select" what do we need. Under the hood it is using our well known useState hook and some subscriptions magic ✨ so when the pets change this component is actually re-rendered, neat! 👌.

Next we encounter some weird "new" syntax in our templates. Remember that the JSX template thing can "open" a JS context with {}. We are using the map function to transform each element inside our pets array to a DOM element. To see which component we need to render I used a simple ternary operator. This pattern is extremely useful to render a collection of elements and it is very similar to ngFor or v-for from Angular and Vue.js.

Finally, I added the key prop to both Cat and Dog components. All React components have this "hidden" prop that you need to use when rendering lists. It is related to some optimization stuff but right now you just need to understand that it is used when rendering collections and it should be unique for each element in the list. I used name there but if we had two pets named "Boira" this will render an error in our console.

Phew... that was a LOT. If you made it until here... congratulations! 🎉You just learn how to use our application state in any component!. Let's see how to modify our state using another hook: useDispatch

Changing our state dispatching actions

As I mentioned before, we cannot change the Redux application state manually (immutability again!). It follows the same principle as the useState hook. We have a piece of data and a function to replace this data. In our case, the mechanism to change the application state is the combination of the useDispatch and our reducer function!

Let's enhance our application state to track how many hugs a pet has received:

// src/pets/store/index.js
export const DOG = 'dog'
export const CAT = 'cat'

export const PETS_INITIAL_STATE = [
  { kind: DOG, name: 'Boira', initialScore: 13, hugs: 0 },
  { kind: DOG, name: 'Black', initialScore: 13, hugs: 0 },
  { kind: CAT, name: 'Uhura', initialScore: 13, hugs: 0 },
]

const pets = (state = PETS_INITIAL_STATE, action) => {
  return state
}

export default pets

We want to add a button to give a hug to our pets! To make the example a bit simpler I am gonna just modify the pets container:

// src/pets/containers/pets/Pets.js
import React from 'react'
import { useSelector } from 'react-redux'

import { DOG } from '../../store'
import Dog from '../../../dogs/components/dog/Dog'
import Cat from '../../../cats/components/cat/Cat'

const Pets = () => {
  const pets = useSelector((state) => state.pets)

  return (
    <>
      {pets.map(({ kind, name, initialScore }) => (
        <div key={name}>
          {kind === DOG ? (
            <Dog key={name} name={name} initialScore={initialScore} />
          ) : (
            <Cat key={name} name={name} initialScore={initialScore} />
          )}
          <button>Give a hug!</button>
        </div>
      ))}
    </>
  )
}

export default Pets

Nothing really new here! I just created some container around the Cat/Dog component and add a simple <button>. Now we are ready to keep adding some stuff to this container but I like to keep things as clean as possible, so I don't want to pollute the container with all the Redux stuff. If possible, I want to be able to interact with our store without knowing anything about Redux (from our container perspective).

This sounds like a new hook! Remember, we can use hooks to add some business logic to our component so let's change the Pets container to use a custom hook:

// src/pets/containers/pets/Pets.js
import React from 'react'

import usePetsStore from '../../hooks/usePetsStore'
import { DOG } from '../../store'
import Dog from '../../../dogs/components/dog/Dog'
import Cat from '../../../cats/components/cat/Cat'

const Pets = () => {
  const petsStore = usePetsStore()
  const pets = petsStore.getAllPets()
  const totalHugs = petsStore.getTotalHugs()

  return (
    <>
      <p>Total hugs: {totalHugs} </p>
      {pets.map(({ kind, name, initialScore }) => (
        <div key={name}>
          {kind === DOG ? (
            <Dog key={name} name={name} initialScore={initialScore} />
          ) : (
            <Cat key={name} name={name} initialScore={initialScore} />
          )}
          <button onClick={() => petsStore.hugPet(name)}>
            Give a hug to {name}!
          </button>
        </div>
      ))}
    </>
  )
}

export default Pets

The previous snippet is going to break your application, but we are going to fix it really soon! Notice I used the usePetsStore custom hook (we have not created it yet) and called a bunch of methods: getAllPets, getTotalHugs and hugPet. These methods will act as a facade between our container and our store. The direct benefit of this is we can change our store without changing anything in our component and it will just work ✨.

I also added a message with the total number of hugs because I want a sane competition between our pets 😼🐶. Let's create our custom hook and use some fake data and methods first!

// src/pets/hooks/usePetsStore.js
const usePetsStore = () => {
  return {
    getAllPets() {
      return [{ kind: 'dog', name: 'Boira', initialScore: 13 }]
    },
    getTotalHugs() {
      return 0
    },
    hugPet(name) {
      console.log(`You are hugging ${name}!`)
    },
  }
}

export default usePetsStore

You can test your application now! It should just work using some fake data and you can even test if the new button is working as expected. Let's restore our poor pets now and give getTotalHugs a proper implementation:

// src/pets/hooks/usePetsStore.js
import { useSelector } from 'react-redux'

const usePetsStore = () => {
  const pets = useSelector((state) => state.pets)

  return {
    getAllPets() {
      return pets
    },
    getTotalHugs() {
      return pets.reduce((hugs, pet) => hugs + pet.hugs, 0)
    },
    hugPet(name) {
      console.log(`You are hugging ${name}!`)
    },
  }
}

export default usePetsStore

You might be wondering... why are not using the useSelector inside the getAllPets function like this 🤔?

// part of src/pets/hooks/usePetsStore.js
getAllPets() {
  return useSelector((state) => state.pets);
},
getTotalHugs() {
  return this.getAllPets().reduce((hugs, pet) => hugs + pet.hugs, 0);
},

If you thought about that... you are right! The previous snippets look like a perfect valid JS code but unfortunately, it doesn't work with React 😔. A hook can only be executed in a component or in a custom hook "function" so we cannot use them inside those methods. Moving the useSelector to the top fixes the issue and it doesn't look that bad 😅.

Let's get back to the main topic of this section: changing our store!. First we need to create an action with a type property. I usually keep those actions in a separate file in the store folder:

// src/pets/store/actions.js
export const HUG_PET = 'HUG_PET'

export const hugPet = (name) => {
  return {
    type: HUG_PET,
    payload: {
      name,
    },
  }
}

As you can see I created a method called hugPet that returns an action. This kind of methods is called "action creators" because they well… create actions! We used a constant value for the type as this is a common pattern to avoid errors writing plain strings and also added a payload property with the name (remember that you can add as many extra properties as you want). Also be careful when creating those constants since each action type must be unique!

Let's see how we can use this new action in our custom hook:

// src/pets/hooks/usePetsStore.js
import { useSelector, useDispatch } from 'react-redux'

import * as fromActions from '../store/actions'

const usePetsStore = () => {
  const pets = useSelector((state) => state.pets)
  const dispatch = useDispatch()

  return {
    getAllPets() {
      return pets
    },
    getTotalHugs() {
      return pets.reduce((hugs, pet) => hugs + pet.hugs, 0)
    },
    hugPet(name) {
      dispatch(fromActions.hugPet(name))
    },
  }
}

export default usePetsStore

I used the useDispatch custom hook from the react-redux library to get our dispatch function. Using this function is the only way to call our reducers! Then I used our previous action creator to create the action, so we don't need to worry about dealing with the action shape in here 👌. Notice I used a special import here, so I can always use fromActions.whatever to invoke any action creator. The main reason to do that is that we already have a hugPet function in this scope and I think it makes the code a bit more readable 🤓.

If you expected to start giving hugs to your pets...wrong! We need to change our reducer function to update our state when the previous action is dispatched:

// src/pets/store/index.js
import { HUG_PET } from './actions'

export const DOG = 'dog'
export const CAT = 'cat'

export const PETS_INITIAL_STATE = [
  { kind: DOG, name: 'Boira', initialScore: 13, hugs: 0 },
  { kind: DOG, name: 'Black', initialScore: 13, hugs: 0 },
  { kind: CAT, name: 'Uhura', initialScore: 13, hugs: 0 },
]

const pets = (state = PETS_INITIAL_STATE, action) => {
  switch (action.type) {
    case HUG_PET: {
      const { name } = action.payload
      return state.map((pet) => {
        if (pet.name === name) {
          return { ...pet, hugs: pet.hugs + 1 }
        }
        return pet
      })
    }
    default: {
      return state
    }
  }
}

export default pets

Again there is a lot to digest....or maybe if you are familiar with immutability you can read this code without even blinking! (I can't 😅).

Remember that the reducer always receive the current state and returns the next state. I am checking the action type and if it's something that I need to handle I do something. Otherwise, I just return the same state.

It is essential to always create a new object when returning the next state so that's the main reason to use a map function: I am transforming the original array to another array. Inside the map function I am just checking if the pet's name is the same as the one I am receiving from the action's payload and updating the hugs count (again creating a new object using the spread operator). If it's not the pet I am looking for I just return it.

Let's hug all the pets! 🥰😸

Conclusion

Today we covered a lot! We started adding the Redux library to our application and created an application state. Then we added more properties to our pets and now we can hug them! In the next article we are going to deal with asynchronous operations and create a small mini-game, you will see.

Cover photo by Barn Images

View all posts tagged as