Super Simple Redux Thunk Example

This Redux Thunk example is the bare minimum it takes to get a React application up and running with asynchronous redux actions. It is under 100 lines and consists of just two files (with dependencies).

These two files will show you start to finish how to make a redux store, create some async/sync actions, and connect them to a component.

It's the simplest example of the entire React, Redux, Redux Thunk lifecycle I could come up with.

This post is also a follow up to Super Simple React Redux Example this post and the previous one are very similar so you can flip back and forth to see the difference between Redux with and without thunks. This should make it much easier to determine what is going on.

Setup

npm install -g create-react-app  
create-react-app my-simple-async-app  
cd my-simple-app  
npm install --save redux react-redux redux-thunk  
echo '' > src/redux.js  

OR... you can clone my repo where I've already done the steps above and below for you:

git clone git@github.com:tylerbuchea/my-simple-async-app.git  
cd my-simple-async-app  
npm install  
npm start  

You can also view the working simple async app here: https://codesandbox.io/s/github/tylerbuchea/my-simple-async-app

redux.js

This file contains the store, actions, and reducers all wrapped up into one. For more advanced applications with more actions you would break this file up into parts.

I've put file name comments above each section to illustrate where they would go if you were to break them up into separate files.

We're also going to use Redux Thunk to asynchronously fetch the most recently updated repos by username from Github using this REST URL: https://api.github.com/users/tylerbuchea/repos?sort=updated

You can check out the Github API documentation on the repos endpoint here: https://developer.github.com/v3/repos/

import { applyMiddleware, combineReducers, createStore } from 'redux';

import thunk from 'redux-thunk';

// actions.js
export const addRepos = repos => ({  
  type: 'ADD_REPOS',
  repos,
});

export const clearRepos = () => ({ type: 'CLEAR_REPOS' });

export const getRepos = username => dispatch =>  
  fetch(`https://api.github.com/users/${username}/repos?sort=updated`)
    .then(response => response.json())
    .then(responseBody => dispatch(addRepos(responseBody)))
    .catch(() => dispatch(clearRepos()));

// reducers.js
export const repos = (state = [], action) => {  
  switch (action.type) {
    case 'ADD_REPOS':
      return action.repos;
    case 'CLEAR_REPOS':
      return [];
    default:
      return state;
  }
};

export const reducers = combineReducers({ repos });

// store.js
export function configureStore(initialState = {}) {  
  const store = createStore(reducers, initialState, applyMiddleware(thunk));
  return store;
}

export const store = configureStore();  

Take note of the Redux Thunk specific pieces above. First notice the applyMiddleware(thunk) call. This tells redux to accept and execute functions as return values. Redux usually only accepts objects like { type: 'ADD_THINGS', things: ['list', 'of', 'things'] }.

But now that you've applied the Redux Thunk middleware you can return a function. The middleware checks if the action return is an object our function if it's a function it will execute the function and inject a callback function called dispatch. This way you can start an asynchronous task and then use the dispatch callback to return a regular redux object action some time in the future. Think of it like this:

// This is your typical redux sync action
function syncAction(listOfThings) {  
  return { type: 'ADD_THINGS', things: listOfThings  }
}

// This would be the async version
// where we may need to go fetch the
// list of things from a server before
// adding them via the sync action
function asyncAction() {  
  return function(dispatch) {
    setTimeout(function() {
      dispatch(syncAction(['list', 'of', 'things']));
    }, 1000);
  }
}

App.js

I've put the React component and the Redux "container" connection logic into one file for clarities sake. If you want you can break the two sections up into separate files by referencing the filename comments I've added.

import React, { Component } from 'react';

import { connect } from 'react-redux';

import { getRepos, clearRepos } from './redux';

// App.js
export class App extends Component {  
  state = { username: 'tylerbuchea' };

  componentDidMount() {
    this.updateRepoList(this.state.username);
  }

  updateRepoList = username => this.props.getRepos(username);

  render() {
    return (
      <div>
        <h1>I AM AN ASYNC APP!!!</h1>

        <strong>Github username: </strong>
        <input
          type="text"
          value={this.state.username}
          onChange={ev => this.setState({ username: ev.target.value })}
          placeholder="Github username..."
        />
        <button onClick={() => this.updateRepoList(this.state.username)}>
          Get Lastest Repos
        </button>

        <ul>
          {this.props.repos.map((repo, index) => (
            <li key={index}>
              <a href={repo.html_url} target="_blank">
                {repo.name}
              </a>
            </li>
          ))}
        </ul>

      </div>
    );
  }
}

// AppContainer.js
const mapStateToProps = (state, ownProps) => ({ repos: state.repos });  
const mapDispatchToProps = { getRepos, clearRepos };  
const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App);

export default AppContainer;  

index.js

You'll also need to update your index React component to connect Redux to your application. It is a simple two step process and looks like this:

import React from 'react';  
import ReactDOM from 'react-dom';  
import App from './App';  
import './index.css';

// Add these imports - Step 1
import { Provider } from 'react-redux';  
import { store } from './redux';

// Wrap existing app in Provider - Step 2
ReactDOM.render(  
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);