REDUX-THUNK MIDDLEWARE

Agenda

We often need to send or get data from a database. Now that we are implementing Redux in our applications, let’s take a look at how middleware can help us deal with our asynchronous Redux code.

Once we fetch our data, we no longer want to store it in our component’s state. We want to be able to put it in the Redux store when it comes back.

Today, we will:

  • Discuss what middleware is, and what it is used for
  • Learn about Thunks, and the problems they solve for us
  • Explore a familiar project, and discuss where we could use thunks
  • Write some thunks

Learning goals

  • Be able to explain why middleware is helpful
  • Be able to add middleware to your redux project
  • Be able to write your own thunks
  • Be able to test thunks

Vocab

  • Middleware
  • Thunks
  • redux-thunk

What is Middleware?

Middleware provides a 3rd party between dispatching an action and the moment it reaches a reducer. It basically allows us to hook into the lifecycle of Redux and run some other code between the time an action is dispatched and the time it gets to the reducer.

If we think about it, there are 3 stages of an asynchronous request:

  • The start of the request
  • If the request succeeds
  • If the request fails

Our state needs to account for each of these stages.

Thunk, n.

A thunk is just another name for a function. But it’s not just any old function… it’s a special name for a function that wraps an expression to delay its evaluation. Let’s take a look at a very basic example of a thunk.

const notAThunk = () => {
   return () => console.log('do stuff now')
}

Here, the inner function that is returned is a thunk. You’ve probably seen this pattern before; you just didn’t know it was a thunk. If you want to execute the do stuff now part, you would have to call it like notAThunk()() - calling it twice, basically.

Redux-Thunk

Up until this point, we’ve only seen Redux actions as objects that don’t do anything. Pretty boring, right? Wouldn’t it be cool if we could actually make them do something… like make a fetch request or trigger other actions? Enter redux-thunk!

Redux-Thunk is a middleware that allows our action creators to return a function instead of an action object. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. It looks at every action that passed through the system; and if it’s a function, it calls that function. This is just another tool that can help us decouple our application logic from our view rendering.

Redux passes 2 arguments to thunk functions: dispatch so that we can dispatch new actions if we need to, and getState so that we can access the current state.

Enough talk - let’s see how this actually works!

To demonstrate how redux-thunk actually works, we’re going to be using this same repos we used to create our Turing Front-End Staff website. Here is the client-side repo we will be working in and the backend repo we will be fetching our data from.

Start Up Instructions

  • Clone down promises-api, run npm install and npm start. The server should now be running on localhost:3001
  • Clone down promises-practice
  • Checkout the branch pre-redux-aa git checkout pre-redux-aa
  • Install the dependencies npm install
  • Start up the application npm start

If we take a look at App.js, we can see that our component’s state currently has 3 properties. These properties correspond to the 3 stages of our async request that we need to account for.

// App.js
this.state = {
   staff: [],
   isLoading: false,
   error: ''
}
Take a few minutes and review the fetchStaff, fetchBios, and componentDidMount methods that are being used to fetch our data and handle our loading and error cases.

Converting to Redux

To start, we will need to add Redux, React-Redux, and Redux-Thunk as dependencies of our project so that we can use them. I’m also going to go ahead and install the redux-devtools-extension. I have found that this is easier to use when passing multiple arguments to createStore().

npm install --save redux react-redux redux-thunk redux-devtools-extension

Designing our state

From what we already have, we know our state needs to have 3 properties:

  • staff
  • isLoading
  • error

We will need to create an action for each of these, but we will probably also need an additional 2 action creators that will call our other 3 action (creators) depending on the status of fetching the data. These additional 2 action creators will look very similar to our asynchronous fetchStaff and fetchBios methods, but instead of directly setting state with this.setState({ isLoading: true }), we’ll dispatch an action to do the same: dispatch(isLoading(true)).

Creating our actions

Let’s create an actions folder with an index.js to hold our synchronous actions and a thunks folder to hold our asynchronous actions. Let’s start with our 3 simple synchronous actions that we know we will need:

// actions/index.js

export const isLoading = (bool) => ({
   type: 'IS_LOADING',
   isLoading: bool
})
	
export const hasErrored = (message) => ({
   type: 'HAS_ERRORED',
   message
})

export const setStaff = (staff) => ({
   type: 'SET_STAFF',
   staff
})

Now that we have the 3 actions that will represent the state of our network request, we need to create our other 2 action creators that will reflect our fetchStaff and fetchBios methods. By default, Redux action creators don’t support async actions like fetching data, so here’s is where we will utilize the redux-thunk middleware. We can transition fetchStaff and fetchBios into asynchronous action creators by returning a function instead of an object. The functions that we return from fetchStaff and fetchBios can safely perform a network request and dispatch a synchronous action with the response data.

Let’s make a separate file for each of our thunk action creators (it will make them easier to test down the road!). We will also need to import any actions that we might need to dispatch.

// thunks/fetchStaff.js

import { isLoading, hasErrored, setStaff } from '../actions'
import { fetchBios } from './fetchBios.js'

export const fetchStaff = (url) => {
  return async (dispatch) =>  {
    try {
      dispatch(isLoading(true))
      const response = await fetch(url)
      if(!response.ok) {
        throw Error(response.statusText)
      }
      const data = await response.json()
      const staff = await dispatch(fetchBios(data.bio))
      dispatch(isLoading(false))
      dispatch(setStaff(staff))
    } catch (error) {
      dispatch(hasErrored(error.message))
    }
  }
}
// thunks/fetchBios.js

import { hasErrored } from '../actions'

export const fetchBios = (staffArray) => {
  return (dispatch) => {
    const unresolvedPromises = staffArray.map(async staffMember => {
      try {
        const response = await fetch(staffMember.info)
        if(!response.ok) {
          throw Error(response.statusText)
        }
        const data = await response.json()
        return { ...data, name: staffMember.name}
      } catch (error) {
        dispatch(hasErrored(error.message))
        }
      })
    return Promise.all(unresolvedPromises)
  }
}

Creating our reducers

Now that we have all our action creators defined, we need to write our reducers that take our actions and return a new copy of our state. This should be nothing new.

// reducers/staffReducer.js

export const isLoading = (state = false, action) => {
  switch(action.type) {
    case 'IS_LOADING':
      return action.isLoading
    default:
      return state
  }
}

export const hasErrored = (state = '', action) => {
  switch(action.type) {
    case 'HAS_ERRORED':
      return action.message
    default:
      return state
  }
}

export const staff = (state = [], action) => {
  switch(action.type) {
    case 'SET_STAFF':
      return action.staff
    default:
      return state
  }
}

Now we need to combine our individual reducers into a rootReducer to create a single object that can be passed to our createStore() method.

import { combineReducers } from 'redux';
import { isLoading, hasErrored, staff } from './staffReducer';

const rootReducer = combineReducers({
  staff,
  isLoading,
  error: hasErrored
})

export default rootReducer;

Create our store and provide it to our app

This is great! All that’s left to do is head over to our index.js and configure our store, pass it to our application, and then connect our component to the store. This is where we will tell the store about redux-thunk and it will give us access to dispatch in our action creators.

// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import rootReducer from './reducers';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';


const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>, document.getElementById('root'));

Now we just need to clean up our App component and allow it to use the Redux store and methods. We can completely get rid of our constructor and component state as well as our fetchStaff and fetchBios methods. Now let’s map some props and connect to the store!

// App.js

const mapStateToProps = (state) => ({
  staff: state.staff,
  isLoading: state.isLoading,
  error: state.error
})

const mapDispatchToProps = (dispatch) => ({
  fetchStaff: (url) => dispatch(fetchStaff(url))
})

export default connect(mapStateToProps, mapDispatchToProps)(App);

Previously, we had destructured staff, isLoading, and error off of state. We now are destructuring them off of props. Lastly, we just need to call this.props.fetchStaff(url) in componentDidMount().

Testing Thunks

So, think back to last week when we were testing mapDispatchToProps… What were we actually testing? Testing thunks is going to be very similar. We don’t actually want to test dispatch (we didn’t write dispatch). So we’re probably going to want to mock dispatch and just test that it was called with the correct action. That doesn’t sound so bad, right?

Let’s start with fetchStaff. What is the first action that gets dispatched? Are we doing anything asynchronous at this point? Nope! We’re just dispatching isLoading right before we kick off our network request. We already said we were going to mock dispatch, so the only other mock we need is just a url.

First things first… We need to import fetchStaff and fetchBios (it gets called in fetchStaff) and all of our synchronous actions that get dispatched (isLoading, hasErrored, and setStaff). We can also go ahead and create any mocks that we are going to need.

With the help of redux-thunk, when we call fetchStaff with our mockUrl, we are returned a function that then takes dispatch as an argument. We then call that function, passing it our mockDispatch. Now we can expect that our mockDispatch was called with isLoading(true).

// thunks/__tests__/fetchStaff.js

import { fetchStaff } from '../fetchStaff'
import { fetchBios } from '../fetchBios'
import { isLoading, hasErrored, setStaff } from '../../actions'

describe('fetchStaff', () => {
  let mockUrl
  let mockStaff
  let mockDispatch
  
  beforeEach(() => {
    mockUrl = 'www.someurl.com'
    mockStaff = [
      {name: 'Christie', info: 'www.somemoreinfo.com'}, 
      {name: 'Will', info: 'www.somemoreinfo.com'}
    ]
    mockDispatch = jest.fn()
    window.fetch = jest.fn().mockImplementation(() => Promise.resolve({
      ok: true,
      json: () => Promise.resolve({
        bio: mockStaff
      })
    }))
  })
  
  it('calls dispatch with isLoading(true)', () => {
    const thunk = fetchStaff(mockUrl) // this is the inner function that is returned
    
    thunk(mockDispatch)
    
    expect(mockDispatch).toHaveBeenCalledWith(isLoading(true))
  })
  
  it('calls fetch with the correct param', () => {
    const thunk = fetchStaff(mockUrl)

    thunk(mockDispatch)

    expect(window.fetch).toHaveBeenCalledWith(mockUrl)
  })
})

Ok, here’s where we get into async land. We’ve kicked off our network request and now need to test what gets dispatched if the response is ok/not ok. If you need a refresher on how to mock fetch or resolve a Promise in our tests, take some time to go back and review the Testing Async Javascript & API Calls lesson.

// thunks/__tests__/fetchStaff.js

it('should dispatch hasErrored with a message if the response is not ok', async () => {
  window.fetch = jest.fn().mockImplementation(() => Promise.resolve({
    ok: false,
    statusText: 'Something went wrong'
  }))
  
  const thunk = fetchStaff(mockUrl) // again, this is the inner function that is returned
  
  await thunk(mockDispatch)
  
  expect(mockDispatch).toHaveBeenCalledWith(hasErrored('Something went wrong'))
})



it('should dispatch isLoading(false) if the response is ok', async () => {
  const thunk = fetchStaff(mockUrl) // inner function
  
  await thunk(mockDispatch)
  
  expect(mockDispatch).toHaveBeenCalledWith(isLoading(false))
})

We’ve made our initial fetch and gotten back a good response. Now it’s time to dispatch our other asynchronous action creator / thunk. We don’t necessarily care what fetchBios is doing. We are just concerned about whether or not it got dispatched. Since that’s the case, we can just mock out fetchBios.

Remember back when we were creating our thunks and we decided to put each of them in their own file? This is why we did that… we wanted to be able to mock fetchBios individually. So, now let’s go create a manual mock of fetchBios.

// thunks/__tests__/fetchStaff.js

jest.mock('../fetchBios') // this is the file path for the original, not the mock

it('should dispatch fetchBios with the correct param', async () => {
  const thunk = fetchStaff(mockUrl)
  
  await thunk(mockDispatch)
  
  expect(mockDispatch).toHaveBeenCalledWith(fetchBios(mockStaff))
})

Only 1 test left for fetchStaff

// thunks/__tests__/fetchStaff.js

it('should dispatch setStaff with the correct params', async () => {
  const finalStaff = [
    {name: 'Christie', bio: 'Christie bio', image: 'Christie image'}, 
    {name: 'Will', bio: 'Will bio', image:   'Will image'}
  ]
  
  const thunk = fetchStaff(mockUrl)
  
  mockDispatch = jest.fn().mockImplementation(() => finalStaff)

  await thunk(mockDispatch)

  expect(mockDispatch).toHaveBeenCalledWith(setStaff(finalStaff))
})

YOUR TURN! Pair up with a partner and see if you can write the tests for fetchBios.

Resources

Lesson Search Results

Showing top 10 results