Redux

Redux is a well-known library that does state management for you, very similarly to how we used context. With context, you use the provider and consumer as a sort of portal to skip passing parameters through every component. With Redux, we're taking the state management out of React entirely and moving it to a separate store.

Why do we have Redux?

  1. Context used to be a lot worse to use and less useful. This made Redux (or Redux-like) management tools the only option
  2. Redux code is extremely testable. This is probably the most compelling reason to use it. Having your state mutation be broken up in such a way to make it easy to test is fantastic. This is also mitigated because we have useReducer now.
  3. The debugging story is pretty good.

So given that we do now have the next context API, how often will I use Redux? Never, I anticipate. I rarely had problems that Redux solved (they exist; I just didn't have them) and the few cases now where I would see myself using Redux I think React's context would cover it. But if Redux speaks to you, do it! Don't let me stop you. It's a great library. Just be cautious. And there are reasons to use it: if you have complex orchestrations of async data, Redux can be immensely useful and I would use it for that.

Okay, let's get started. React state management is pretty simple: call setState and let React re-render. That's it! Now there's a few steps involved.

  1. User types in input box
  2. Call action creator to get an action
  3. Dispatch action to Redux
  4. Redux inserts the action into the root reducer
  5. The root reducer delegates that action to the correct reducer
  6. The reducer returns a new state given the old state and the action object
  7. That new state becomes the store's state
  8. React is then called by Redux and told to update

So what was one step became several. But each step of this is testable, and that's great. And it's explicit and verbose. It's long to follow, but it's an easy breadcrumb trailer to follow when things go awry. So let's start writing it:

Run npm install redux react-redux. Create store.js and put in it:

import { createStore, compose, applyMiddleware } from "redux";
import reducer from "./reducers";

const store = createStore(
  reducer,
  typeof window === "object" &&
    typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== "undefined"
    ? window.__REDUX_DEVTOOLS_EXTENSION__()
    : f => f
);

export default store;

We're including the dev tools middleware (I'll show you at the end) as well as redux-thunk which we'll use in a second to do async actions. This is the base of a store: a reducer. A store is just basically a big object with prescribed ways of changing it. So let's go make our first reducer.

Make a new folder in src called reducers. Create a file called index.js in reducers and put:

import { combineReducers } from "redux";
import location from "./location";

export default combineReducers({
  location
});

combineReducers is a convenience function from Redux so you don't have to write your own root reducer. You can if you want to; this is just a bit easier. So now we have a root reducer that will delegate all changed to the location key to this reducer. So let's go make it. Make a file called location.js and put in it:

export default function location(state = "Seattle, WA", action) {
  switch (action.type) {
    case "CHANGE_LOCATION":
      return action.payload;
    default:
      return state;
  }
}

Not very difficult. A reducer takes an old state, an action, and combines those things to make a state. In this case, if the state is San Francisco, CA and some calls it with the action {type: 'CHANGE_LOCATION': payload: 'Salt Lake City, UT' } then the new state location would be Salt Lake City, UT.

A reducer must have a default state. In our case, using ES6 default params, we made Seattle, WA our default state. This is how Redux will initialize your store, by calling each of your reducers once to get a default state.

The shape of the action object is up to you but there is a thing called Flux Standard Action that some people adhere to to make building tools on top of actions easier. I've not used any of those tools but I also don't have a good reason not to use this shape so I do. In sum, make your action shapes be { type: <[String, Number], required>, payload: <any?>, error: <any?>, meta: <any?> }. The type could in theory be a Symbol too but it messes up the dev tools.

Reducers are synchronous: they cannot be async. They also must be pure with no side-effects. If you call a reducer 10,000,000 times with the same state and action, you should get the same answer on the 10,000,001st time.

Okay, so now we understand how, once given a state and an action, we can make a reducer. We haven't made nor dispatched those actions yet but we're getting there. Let's make the other reducers.

theme.js

export default function theme(state = "darkblue", action) {
  switch (action.type) {
    case "CHANGE_THEME":
      return action.payload;
    default:
      return state;
  }
}

index.js

import { combineReducers } from "redux";
import location from "./location";
import theme from "./theme";

export default combineReducers({
  location,
  theme
});

Let's go make the action creators. These are the functions that the UI gives to the store to effect change: actions. These functions create actions.

Create a new folder called actionCreators and put in changeTheme.js

export default function changeTheme(theme) {
  return { type: "CHANGE_THEME", payload: theme };
}

That's it! This one is the simplest form: create an object and return it. Some people will inline these action shapes in their React components. I prefer this because it makes refactors simple. Let's make the other two:

changeLocation.js

export default function changeLocation(location) {
  return { type: "CHANGE_LOCATION", payload: location };
}

That's it for action creators. In previous versions of this course, I taught how to do async actions so [check this out if you want to see that][v4-async]. there are a thousand flavors of how to do async with Redux. The most popular are redux-observable, redux-saga, redux-promise, and redux-thunk. I showed how to use redux-thunk because it's simplest: the others are more powerful but more complex.

Okay, let's go integrate this now where context was being used before. Go to App.js:

// delete ThemeContext, useState import

// import
import { Provider } from "react-redux";
import store from "./store";

// delete useState call

// wrap app with
<Provider store={store}>[]</Provider>;

Feels nice deleting a lot of code, right?

Just like context makes your store available anywhere in your app, so does Provider.

Now that Redux is available everywhere, let's go add it to SearchParams.js

// replace ThemeContext import
import { connect } from "react-redux";

// replace context references
location: this.props.location,
animal: this.props.animal,
breed: this.props.breed,

// replace export
const mapStateToProps = ({ theme, location }) => ({
  theme,
  location
});

export default connect(mapStateToProps)(SearchParams);

Connect is a little helper that will pluck things out of state and put them into your props for you so you can reference those as if they were normal state. This makes it not too hard to keep your Redux and React separate too so you can test both independently.

We're not quite done here. We can got the reading part of Redux done but now the writing. Let's go do that too in SearchParams.js

//replace Consumer import
import changeLocation from "./actionCreators/changeLocation";
import changeAnimal from "./actionCreators/changeTheme";

// destructure the methods from props
const SearchParams = ({ theme, location, setTheme, updateLocation }) => {
  /* code */
});

// at the bottom
const mapDispatchToProps = dispatch => ({
  updateLocation(location) {
    dispatch(changeLocation(location));
  },
  setTheme(theme) {
    dispatch(changeLocation(theme));
  }
});

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

Now we're also using mapDispatchToState which lets us write functions to dispatch actions and thunks to Redux. Let's quickly add it to Details.js

// replace ThemeContext import
import { connect } from "react-redux";

// remove all the ThemeContext stuff and the interior function
// replace `context.theme` with just `this.props.theme` for the backgroundColor

// bottom
const mapStateToProps = ({ theme }) => ({ theme });

const WrappedDetails = connect(mapStateToProps)(Details);

// replace <Details />
<WrappedDetails {...props} />;

Now it should work! Redux is a great piece of technology that adds a lot of complexity to your app. Don't add it lightly. I'd say you'd rarely want to start a new project using Redux: hit the problems first and then refactor it in. You just saw how.

Let's quickly try the dev tools:

Download the one you're using, open up your app, and mess around the Redux tab. You can time travel, auto-generate tests, modify state, see actions, all sorts of cool stuff. Another good reason to use Redux.

Hopefully you're well informed on the boons and busts of introducing Redux. It's great, just be careful.

If you want a deeper dive, check out the Frontend Masters course on Redux!

🌳 branch redux