Practical React & Redux - Part 4

This is the final part of the React workshops series created by our team. Let's learn how to deal with asynchronous operations.

This is the final part of the React workshops series created by our team. Today we are going to finish our simple React application adding an asynchronous operation and converting the app into a simple game.

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

Introduction

Our application looks great, and we have our state in a single place thanks to Redux. The problem is in a real application you usually need to deal with asynchronous operations like external APIs for example.

Today we are going to enhance the app and convert it to a simple game where you will be able to hug the pets in turns.

Using an API an dealing with asynchronous code

Our pets need a lot of attention, and we cannot hug them all! Let's make them wait turns and hug one of them at a time. I don't want to have the responsibility to decide which pet can receive hugs at a time, so we are going to use an API to generate random numbers instead (a bit overengineered but... I really cannot decide between them 😭).

First of all, let's enhance our application state and add a huggable property for our pets:

// 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, huggable: false },
  { kind: DOG, name: 'Black', initialScore: 13, hugs: 0, huggable: false },
  { kind: CAT, name: 'Uhura', initialScore: 13, hugs: 0, huggable: false },
]

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

Then use that property to enable/disable the "Give hug" buttons:

// 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, huggable }) => (
        <div key={name}>
          {kind === DOG ? (
            <Dog key={name} name={name} initialScore={initialScore} />
          ) : (
            <Cat key={name} name={name} initialScore={initialScore} />
          )}
          <button onClick={() => petsStore.hugPet(name)} disabled={!huggable}>
            Give a hug to {name}!
          </button>
        </div>
      ))}
    </>
  )
}

export default Pets

As you can see it was straightforward! We just used the disabled prop and pass them the previous boolean value so all buttons should be disabled by now.

Now it's the time to change our application state and you already know we cannot do that without creating an action. Let's create a new action using a new action creator called chooseNextHuggablePet:

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

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

export const getRandomPet = (pets) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      const petChosen = pets.find((pet) => pet.name === 'Boira')
      resolve(petChosen)
    }, 1000)
  })
}

export const chooseNextHuggablePet = async (pets) => {
  const petChosen = await getRandomPet(pets)
  return {
    type: CHOOSE_HUGGABLE_PET,
    payload: {
      petChosen,
    },
  }
}

I know... it's not that fair! πŸ˜‚ I promise you this is just a boilerplate code that we will replace with a fair solution in a few minutes. I kind of emulated a delayed response using a Promise with a setTimeout function. After one second we are returning a pet named "Boira" 🐢. Let's use this new action in our pets container!

The idea is to choose a new pet every 15 seconds. We are going to learn how to use another React built-in hook: the useEffect hook! We can use this hook to call a function when a component is created so it is a good place to set up our timer function:

// src/pets/containers/pets/Pets.js
import React, { useEffect } 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()

  useEffect(() => {
    const intervalId = setInterval(() => {
      petsStore.chooseNextHuggablePet()
    }, 15_000)
    petsStore.chooseNextHuggablePet()
    return () => {
      clearInterval(intervalId)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

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

export default Pets

The previous code is not gonna work yet because we haven't created the method inside our usePetsStore custom hook. Let's focus on the useEffect hook first! As you can see the useEffect receives two params: the first one is the function that will be executed when the component has been created, the second one is a list of variables that will trigger the previous function if some value change. In our case, I don't want to add any variable but eslint is warning us to be exhaustive and add the petsStore variable. Adding petsStore will cause a serious bug in our application, so I am just disabling the eslint recommendation for that line.

Another interesting thing is we are returning a function in that callback function πŸ˜…. That's pretty common because in a useEffect we usually create subscriptions or timers that we need to clean when the component is destroyed. This is essential to avoid memory leaks! 🚰

After setting the interval I am just executing the method right away to choose our first pet. Let's continue with the implementation of our usePetsStore 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))
    },
    chooseNextHuggablePet() {
      dispatch(fromActions.chooseNextHuggablePet(pets))
    },
  }
}

export default usePetsStore

Nothing fancy here! Just using dispatch again with our previous action creator. I am also passing it the list of the pets that we collected using the useSelectorhook. Let's hug our pets. Oh... you get an error, right? That's "normal" let me explain πŸ˜….

We can only dispatch things that are actions (remember, an object with a type property) but our action creator is not returning an action, right? It is a bit subtle because I used the async/await syntax sugar, but we are returning a Promise object under the hood and Redux doesn't know how to handle Promise objects.

We need to extend Redux with something that is called a middleware. Maybe you know what a middleware is in another context and in fact the concept is very similar when it is applied to Redux. A middleware is just a function that transforms the things that we are dispatching and transforms them to another thing!. Lucky for us there is a middleware that understand how a Promise works: redux-thunk. We need to install the package first:

$ npm i redux-thunk

Then we need to add this middleware in our application. Let's change our App.js like this:

// src/App.js
import React from 'react'
import thunk from 'redux-thunk'
import { createStore, combineReducers, applyMiddleware } 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 }), applyMiddleware(thunk))

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

export default App

We added the applyMiddleware call with the middleware that we just installed. Let's reload our application and hug all our pets! Nope, still doesn't work 😸. To make this work we need to dispatch a function instead of a Promise. That means we need to change our action creator to return a function:

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

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

export const getRandomPet = (pets) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      const petChosen = pets.find((pet) => pet.name === 'Boira')
      resolve(petChosen)
    }, 1000)
  })
}

export const chooseNextHuggablePet = (pets) => async (dispatch) => {
  const petChosen = await getRandomPet(pets)
  dispatch({
    type: CHOOSE_HUGGABLE_PET,
    payload: {
      petChosen,
    },
  })
}

Well maybe I got a bit carried away, sorry! The change might not be very subtle. First I changed the function to return another function! This is that weird syntax: () => () => {}. Let me rewrite this:

export const chooseNextHuggablePet = (pets) => {
  return async (dispatch) => {
    // ...
  }
}

Maybe now looks better, feel free to use whatever form you prefer πŸ‘. The important takeaway here is the chooseNextHuggablePet is returning a function now. In the inner function we have the dispatch argument. This is the same function that we have been using with useDispatch in our usePetsStore custom hook.

Then if you check the function body we are not returning anything now. We are just using dispatch after using our fair random service to get the chosen pet. Feel free to refactor the previous code to use another action creator instead of dispatching the object. I leave this as an exercise for you people!

Finally... still not working! Of course, we forgot to update the reducer function (to be honest I forgot about it while writing this workshop so that's on me). Let's enhance our reducer to update the huggable state for the chosen pet:

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

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

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

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
      })
    }
    case CHOOSE_HUGGABLE_PET: {
      const { petChosen } = action.payload
      return state.map((pet) => {
        return { ...pet, huggable: pet.name === petChosen.name }
      })
    }
    default: {
      return state
    }
  }
}

export default pets

This is very similar to the previous update we made in our reducer. I am checking the action's type and then updating all the pets from our collection. I set the huggable to true if it is the pet that we are looking for. Regarding of the implementation it is essential to clear all the necessary flags when doing things like that so don't forget to set to huggable: false to the other pets!

The application is now working and you can hug all the pets again fairly way! πŸŽ‰

Okay, okay... I promised you to fix this so, let's do it! Back to our actions file:

// src/pets/store/actions.js
import { getRandomPet } from '../services/fair-service'

export const HUG_PET = 'HUG_PET'
export const CHOOSE_HUGGABLE_PET = 'CHOOSE_HUGGABLE_PET'

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

export const chooseNextHuggablePet = (pets) => async (dispatch) => {
  const petChosen = await getRandomPet(pets)
  dispatch({
    type: CHOOSE_HUGGABLE_PET,
    payload: {
      petChosen,
    },
  })
}

I like to keep things separated as possible, so I decided to remove the getRandomPet function from here (since it wasn't an action creator after all) and used a service instead. It's a fancy word to just "encapsulate things" and in our case, since I don't have some state to manage, I decided to use a plain ES Module and not a class.

Let's add the implementation:

// src/pets/services/fair-service.js
const BASE_URL = 'https://www.random.org/integers'

const buildRandomServiceUrl = (max) => {
  return `${BASE_URL}/?num=1&min=0&max=${max}&col=1&base=10&format=plain&rnd=new`
}

const fallBackRandomPet = (pets) => {
  return pets[Math.ceil(Math.random() * pets.length - 1)]
}

export const getRandomPet = async (pets) => {
  try {
    const response = await fetch(buildRandomServiceUrl(pets.length - 1))
    if (response.ok) {
      const random = await response.json()
      return pets[random]
    }
    return fallBackRandomPet(pets)
  } catch (e) {
    console.error(`Something bad happened: ${e}`)
    // Adding a fallback in case the previous request fails
    return fallBackRandomPet(pets)
  }
}

I promise you we have a really fair system now! Let's do a quick walkthrough of the code even it's just vanilla JS. I am using the fetch function to do an AJAX request to https://www.random.org. Passing the correct path and parameters we can get an integer between 0 and pets.length - 1 (the range is inclusive so for a list of 3 elements I want 0, 1 and 2 because the array index starts at 0).

If the response is ok I extract the value using the json method. If the response is not ok, or we raise an error we fallback to another fair method that uses the Math standard library to generate a random number.

If you reload the application you can start hugging the pets as much as you want! Remember that you can only hug one pet at a time! πŸΆπŸ˜ΈπŸ•Ή

Conclusion

You made it, congratulations! This is everything we wanted to teach you with our React and Redux workshop and I hope you enjoyed it! Stay tuned for more workshops created by Codegram.

Cover photo by Adam Patterson

View all posts tagged as

Our Ideation Sprint helps us identify the real business problems and point to a solution