Redux Thunk

Redux Thunk

This post assumes you already have learned and understand the basic concepts of Redux - action creators and actions, the dispatcher, the reducers, and the store. If you're still hazy on some of those, check out our post on Redux and get some practice in first.

What does Thunk help us do?

Problem: Action creators are functions that return objects immediately, and when the dispatcher calls them, it sends them to the reducer immediately, which updates the store... immediately. This works great for things that are completely local to the user's machine and can happen without delay.

But sometimes an action requires time, like when you're using HTTP to interact with a server.

Solution: Thunk!

Thunk (npm install --save redux-thunk) allows us to have our action creators return a callback function instead of an object, which eventually will return the action object, but only when the time is right (such as after an HTTP call completes).

How to use Redux Thunk

The main changes we'll make will be to our action creators themselves - we'll write them so they return the callback function. However, vanilla Redux doesn't understand how to handle action creators that don't return action objects. (Those objects with a type property.) Fortunately, with Redux we can add middleware to the store which makes sure that it learns how to handle certain new features we want it to learn. It's essentially allowing us to upgrade Redux for those times where we need to slow things down a bit due to asynchronicity in our application.

Adding Thunk as middleware

Adding redux-thunk as middleware to our store is pretty simple. Import applyMiddleware from the Redux library and thunk from redux-thunk. Then use applyMiddleware to make sure Redux knows to use thunk when dispatching actions:

// ...

import thunk from "redux-thunk";
import { createStore, applyMiddleware } from "redux";

// ...

const store = createStore(reducer, applyMiddleware(thunk));

This tells the store "here's the reducer you should use, and here's some middleware functions you'll also need to go through before sending things to the reducer". Those middleware functions "teach" Redux how to handle action creators that return functions instead of objects.

How to write asynchronous action creators

The main change we need to make now is in how we write action creators. We can't write them to return objects immediately anymore but instead to return functions that directly dispatch the action objects when they're ready to do so.

The function we return takes the dispatch callback function as a parameter. This is a wrapper around the store's store.dispatch method, and it takes the action object directly and sends it to the reducer.

import axios from "axios"
const todosUrl = "https://api.vschool.io/ben/todo/"

export function loadTodos() {

    // Instead of returning an object like we were with plain Redux, 
    // we instead need to return a function. That function will take 
    // the dispatch function so that it can send a dispatch signal to 
    // the reducer, but only at the correct time. Below, the "correct time" 
    // is when our HTTP call finishes and we got a successful response 
    // back. (Note that we DON'T dispatch if there was an error of some 
    // kind. This is because our reducer should only get called if
    // there is data to send to the reducer. We don't want to bother 
    // the reducer if there's nothing to update due to an HTTP error).
    return function(dispatch) {
        axios.get(todosUrl)
            .then(response => {
                dispatch({
                    type: "LOAD_TODOS",
                    todos: response.data
                })
            })
            // We can do whatever we want with this error (it's probably 
            // better to dispatch an error handling action than to just
            // console.log something, but for brevity that's all we're
            // going to do here for now)
            .catch(err => {
                console.log(err)
            })
    }
}

// The rest of these are written with slightly shorter syntax, 
// but are doing the same thing as above
// Note we start using arrow functions below to take 
// advantage of implicit returns
export function addTodo(newTodo) {
    return dispatch => {
        axios.post(todosUrl, newTodo)
            .then(response => dispatch({ type: "ADD_TODO", newTodo }))
            .catch(err => console.log(err))
    }
}

export function deleteTodo(id) {
    return dispatch => {
        axios.delete(todosUrl + id)
            .then(response => dispatch({ type: "DELETE_TODO", id }))
            .catch(err => console.log(err))
    }
}

export function editTodo(editedTodo, id) {
    return dispatch => {
        axios.put(todosUrl + id, editedTodo)
            .then(response => dispatch({ type: "EDIT_TODO", todo: response.data }))
            .catch(err => console.log(err))
    }
}

Essentially all we've done so far is install redux-thunk as middleware to our store and write our action creators to return callback functions instead of objects. Essentially everything else in the application will be handled by the middleware - binding the action creators to state happens automatically with the connect method in our component, our reducer can be written in the same way as we would have before, etc.

Here's an example reducer we could use from the above action creators:

// This syntax assumes you're using `combineReducers`
// to destructure your reducer logic into functions that
// each control smaller pieces of state
function reducer(prevTodos = [], action) {
    switch(action.type) {
        case "LOAD_TODOS":
            return action.todos
        
        case "ADD_TODO":
            return [...prevTodos, action.newTodo]
        
        case "DELETE_TODO":
            return prevTodos.filter(todo => todo._id !== action.id)
        
        case "EDIT_TODO":
            return prevTodos.map(todo => todo._id === action.id ? action.todo : todo)
        
        default:
            return prevTodos
    }
}

Conclusion

Redux Thunk helps us handle asynchronous actions in Redux, which normally would be hard to do with vanilla Redux alone. By using callback functions instead of returning objects directly, we can wait for async actions to complete before sending an action object to the reducer. Now we have the power to use API calls and save data inside the Redux Store!