« Previous Tutorial Next Tutorial »

In today’s tutorial, we’re going to make a move from synchronous actions to asynchronous actions. This requires making some small but significant changes to our Store setup and our reducers, along with moving our fetch functionality from components to our action files. That’s a lot, so we’re only going to handle a single bit of functionality in this tutorial, and then we’ll convert some more in the next one. I’ve chosen to start with the logout function we just built, because a) it should still be fresh in your memory, and b) it’s pretty self-contained.

Let’s start with a quick fix, though. I’m tired of Nodemon reloading our node server every time we change a .js file that’s intended for the front-end! So, create a file at the top level called nodemon.json. It should look like this:

{
  "verbose": true,
  "ignore": ["src/*"]
}

Save that, then head for a terminal window or command prompt. Kill your server entirely. While we’re here, we also need to install the Redux-Thunk module, which opens up the ability to use asynchronous actions. Do that by typing:

yarn add redux-thunk

and hitting enter. Once that’s installed, restart your server with nodemon yarn start. You should be all set! No longer will the entire express server reset every time you save an action, reducer, etc.

Head back to Sublime and let’s get into React stuff. First we need to create a root reducer file, which is basically just a top-level file the combines all of our reducers. We’re currently doing this right in the Store, but it’s better to do it in a separate file because it’s both cleaner and makes it easier to keep hot-reloading on reducers working. So, in /src/reducers, create a file called index.js, and add this code:

import { combineReducers } from 'redux';
import AuthenticationReducer from '../reducers/authentication';
import ProgressReducer from '../reducers/progress';

const reducers = {
  authentication: AuthenticationReducer,
  progress: ProgressReducer,
};

export default combineReducers(reducers);

Now that we’re exporting this functionality from here, we need to remove it from our Store file and import it there instead, so save this file, open up /src/store/index.js, and let’s get to work. First off, add a new line at the very top of the file to reference the module we just installed:

import thunkMiddleware from 'redux-thunk';

As this implies, Redux-Thunk is middleware for Redux, meaning it runs in the middle of Redux’s various operations. In this case, it’s going to allow us to pass around the dispatch function and call it manually instead of having Redux call it for us. This opens up asynchronous code within the actions because now Redux is no longer in charge of deciding what gets dispatched, and when. Of course, that means we now have to make those decisions. Fortunately, it’s not too tough.

Moving on, we can remove “combineReducers” from the imports on line 2, because we’re now using that in /src/reducers/index.js and won’t need it here anymore. Leave the other three methods, though. We also need to change line 3 a bit. Instead of getting the logger directly from redux-logger, we want their createLogger method instead. This will help us set things up to play nicely with Redux-Thunk.

There should be a blank link between line 3 and 5, separating imports that pull things out of modules from imports that pull directly from our code. Keep that blank line, but above the DevTools import, we need to bring in our combined reducers, so add this code:

import combinedReducers from '../reducers';

We can now also delete lines 7 and 8 since we’re importing combinedReducers instead of importing each reducer manually. So now, your imports should use a total of six lines and look like this:

import thunkMiddleware from 'redux-thunk';
import { applyMiddleware, createStore, compose } from 'redux';
import { createLogger } from 'redux-logger';

import combinedReducers from '../reducers';
import DevTools from '../components/shared/DevTools';

Just below that, on lines 8-11, is the now-duplicate combineReducers code that we no longer want. Nuke that entirely, and replace it with this single line:

const loggerMiddleware = createLogger();

Now we need to adjust our enhancer block to include our thunkMiddleware and loggerMiddleware variables. Here’s the entire block:

const enhancer = compose(
  applyMiddleware(
    thunkMiddleware,
    loggerMiddleware,
  ),
  DevTools.instrument(),
);

The rest of our code stays mostly the same, except we need to change lines 23 and 24 from:

    module.hot.accept('../reducers/progress', () =>
      store.replaceReducer(ProgressReducer),

to:

    module.hot.accept('../reducers', () =>
      store.replaceReducer(combinedReducers),

This will make hot-reloading work with all of our reducers instead of just the progress one, which seems handy.

Save this file and let’s head on to /src/components/shared/HeaderContainer.jsx. This file’s about to get a lot smaller! We’re going to move all of the API stuff over to our action file. The way I would do it is to cut-and-paste, then adjust, so … let’s do that. Grab the entire async logUserOut block, from line 16 to line 49, and cut it. Then open /src/actions/authentication.js and just paste that block into the bottom of the file. Don’t save it yet since we’re not done here, but flip back to /src/components/shared/HeaderContainer.jsx.

Now that we’re not handling actions in our components, we don’t need to mapDispatchToProps, so you can delete lines 24 to 31. You don’t even need to cut ‘em, because we don’t need them in our action file. Just vaporize them, leaving only ashes in the wind and the lamentations of those who loved them to mark their place.

This also means we can edit our final line (which with my spacing should currently be line 24). We still need to connect the component in order to gain access to dispatch, which, again, is provided by the Redux-Thunk middleware, but we can shorten the line a bit since we’re no longer passing mapDispatchToProps. Here’s the new line:

export default connect()(HeaderContainer);

Excellent. Now head back to the top of the file and let’s get rid of some imports we no longer need. Delete line 2, line 4 and line 5 entirely. This should leave you with just the React and connect imports. We need to add one more just below those. Here’s the code:

import { logUserOut } from '../../actions/authentication';

We haven’t finished writing that function yet, but we’ll get there. For now, you may notice a problem: we’re referencing a nonexistent this.logUserOut repeatedly in our class. We can’t just change it to logUserOut because that’s not quite how Redux-Thunk works, so here’s what we need to do. First, between our constructor and render blocks, add the following:

  logUserOutFunction() {
    const { dispatch } = this.props;
    dispatch(logUserOut());
  }

This is why we still need that connect line. It’s injecting dispatch into our props. Handy! OK, now we need to change line 11 to bind that method instead of the old one. Just add “Function” to the two instances of logUserOut like this:

    this.logUserOutFunction = this.logUserOutFunction.bind(this);

And, finally, we need to reference that function on line 22, our call to Header, like this:

      <Header authentication={authentication} logUserOutFunction={this.logUserOutFunction} />

And with that, we’ve reduced the size and complexity of our HeaderContainer file significantly. We need to make a very small change to the Header itself, so save this file, then open up /src/components/shared/Header.jsx.

First things first, ESLint is complaining about the order of our methods, so move renderGreeting down below toggleNavbar. This is just good practice, keeping functions that spit out JSX down close to the render block. With that done, all we need to do is change lines 22 and 23 from:

    const { logUserOut } = this.props;
    logUserOut();

to:

    const { logUserOutFunction } = this.props;
    logUserOutFunction();

Phew … wipe the sweat from your brow, save this file, and let’s head back to /src/actions/authentication.js. At the top of the file, add this code:

import { decrementProgress, incrementProgress } from './progress';

Then leave a blank line, and let’s add some comments to our code for readability. Above your big list of action creator functions, add this:

// Action Creators

Below it, leave an empty line and then add this above the logUserOut function you pasted in:

// Log User Out

Don’t adjust the indentation. Most of this function is actually indented correctly for where we’re going to need it to be. For now, let’s change our first line to this:

export function logUserOut() {

Don’t worry, we’ll be bringing that async back on the very next line, so just below line 13, add the following:

  return async (dispatch) => {

See? Now we have proper indentation for the rest of the function, but we do need to add a semi-colon to line 47, and then add a new close brace on line 48 to close the function. With that done, we can delete lines 15 to 21. We don’t need them anymore, since we’re no longer in a component file and no longer dealing with props. Note that we’re returning a function here. This is necessary for Redux-Thunk to work, and what allows us to push dispatch down to where we need it. Speaking of needing dispatch, let’s change our spinner actions. Remember, Redux is no longer handling that functionality for us, so we have to do it ourselves. Change line 16 to:

    dispatch(incrementProgress());

and line 39 to:

    return dispatch(decrementProgress());

Since that’s the final line of our function, we’re returning it so Redux knows we’re done and it can move on. Our .then block needs to change a bit. We no longer want to return, here, so we need an else. Replace lines 29 to 32:

      if (response.status === 200) {
        return logoutSuccessAction();
      }
      return logoutFailureAction(Error: ${response.status});

with this code:

      if (response.status === 200) {
        dispatch(logoutSuccess());
      } else {
        dispatch(logoutFailure(Error: ${response.status}));
      }

Finally, replace line 35:

      logoutFailureAction(error);

with:

      dispatch(logoutFailure(error));

We’re finished. Save this file and head for a browser. Depending on how much you trust your hot reloaders, you can give the page a refresh or not. We can test by logging in with an authenticated user, and then logging out. Unfortunately this is one of those tutorials where you do a bunch of work just to get to the same result at the user level, but trust me, this will help a ton in keeping things clean and organized in the code.

Next up, we’ll do this same thing with the log in function. See you there!

« Previous Tutorial Next Tutorial »