Once you start working with React in anger, there is a tipping point to be aware of where:

  • the complexity of data flows piles up
  • the same data is being rendered in multiple places
  • the number of state changes blow out

Being able to tackle these problems in a single place is where Redux fits in.

Contents

The Problem

Imagine a fairly deep component hierarchy, starting with your top level App component. Deep down the tree, there are two child components that need to access a common piece of data (e.g. customer data). How should these components access the data they require?

Option 1 lift the state

This option involves placing the state (customer data) to the common ancestor in the component tree. In the worst case, this could be the top level component. It then becomes the burden of each decendent component to pass this state down as props (known as props drilling).

In a large app, this sucks.

  • keeping track of what props are being passed around
  • introducing new components between an existing prop drilling path

Option 2 react context

React context exposes global data and functions from a given React component. To access this published state, a downstream component imports the context and consumes.

The common ancestor parent component, could publish customer data (and functions if desired) via CustomerContext.Provider.

The child components could consume this data via CustomerContext.Consumer.

Option 3 Redux

Provides a centralised store (like a local client side DB).

Any component can connect and query the store.

To change (mutate) data in the store is not directly possible. Instead the requesting component dispatches an action, for example Create Customer, and the data is eventually updated/created. Any components connected to this data, automatically receieve it’s freshest version.

A chat with redux

This dialogue from @housecor helps drive all the moving pieces home:

  • React: Hey CourseAction, someone clicked this “Save Course” button.
  • Action: Thanks React! I will dispatch an action so reducers that care can update state.
  • Reducer: Ah, thanks action. I see you passed me the current state and the action to perform. I’ll make a new copy of the state and return it.
  • Store: Thanks for updating the state reducer. I’ll make sure that all connected components are aware.
  • React-Redux: Thanks for the new data Store. I’ll determine if I should tell React about this change so that it only has to bother with updating the UI when necessary.
  • React: Ooo! Shiny new data has been passed down via props from the store! I’ll update the UI to reflect this!

Container vs Presentation Components

Container (aka smart, stateful or controller view) components can be thought of as the backend of the frontend. They’re concerned with behaviour and marshalling. They’re typically stateful, as they aim to keep the child components fresly rendered with the latest data. A good container component should have very minimal (ideally zero) markup.

Presentation (dumb, stateless or view) components are purely concerned with markup, and contain almost no logic. They are dumb.

Container Presentation
How things work How things look
No markup All markup
Stateful Stateless
Aware of Redux Unaware of Redux
Pass data and actions down Receive data and actions with props
Subscribe to Redux state Read data from props
Dispatch Redux actions Invoke callbacks on props

When you notice that some components don’t use props they receive but merely forward them down…it’s a good time to introduce some container components — Dan Abramov

The Redux Principles

  1. One immutable store. It cant be changed directly. Immutability aids debugging, supports server rendering, and opens up the ability to easily centralise state functionality such as undo and redo.
  2. Actions trigger changes. Components that wish to change state, do so by dispatching an action.
  3. State changes are handled by pure functions, known as reducers.

Conceptually this is publish subscribe. The redux flow visually:

+---------+
| ACTION  |
+----+----+
     |
     v
+----+----+       +-----------+
|  STORE  +<----->+  REDUCER  |
+----+----+       +-----------+
     |
     v
+----+----+
|  REACT  |
+---------+

Actions

An action represents the intent via its type property, the only mandatory field. Other data can be encoded into the action however you please.

{ type: RATE_GYM, rating: 4 }

In this example, rating could be a complex object, a boolean, or there could be several more properties in addition to rating.

Typically actions are made with a factory method known as an action creator:

rateGym(rating) {
  return { type: RATE_GYM, rating: rating }
}

This is good practice as the consuming components don’t need to know about the internals of action structures. For each entity type, its common to have a CRUD (create, retrieve, update, delete) set of action creators.

The Store

A store is created in the entry point of your application (i.e. as it starts up):

let store = createStore(reducer);

The store API is suprisingly simple:

  • store.dispatch(action)
  • store.subscribe(listener)
  • store.getState()
  • replaceReducer(nextReducer)

Immutability

A cornerstone principle of redux, is that state is never modified (mutated), instead a completely new copy of the data is returned.

Benefits of immutable state:

  • Clarity around what is responsible for state changes, everything has a clearly defined reducer. No more debugging trying to track down the piece of code responsible for mutating the state.
  • Performance the job of Redux determining if a state change has occured and notifying React is greatly simplified with an immutable store. Instead of doing complex field level comparisons of each data element before and after, all Redux needs to do is a prevStoreState !== storeState check.
  • Unrivalled debugging because state is never mutated, allows for some incredible innovation in the debugging experience, such as time-travel debugging, undo/redo, skipping individual state actions, replaying the state interactions back. The redux devtools extensions opens this up.

Building complex objects up by hand each time a modification is needed, is not practical. JS does provide a number of ways to copy an object:

  • Object.assign shallow copy. Object.assign({}, state, { role: 'admin' }) assigns a new empty object based on the existing state object, after mixing-in the new role property.
  • The spread operator, whatever is placed on the right is shallow copied const newState = { ...state, role: 'admin' }
  • Immuatable friendly Array methods, such as map, filter, reduce, concat and spread. Avoid push, pop and reverse which mutate.

There is a large ecosystem of libraries available (immer, seamless-immutable, react-addons-update, Immutable.js) for working with data in an immutable compatable way, one popular option is immer:

Create the next immutable state tree by simply modifying the current tree

import produce from "immer";

const user = {
  name: "Benjamin",
  address: {
    state: "New South Wales",
  },
};

const userCopy = produce(user, (draftState) => {
  draftState.address.state = "Victoria";
});

console.log(user.address.state); // New South Wales
console.log(userCopy.address.state); // Victoria

Options for enforcing immutability:

  1. Trust the development team, through training and coding practices.
  2. Warn whenever state is mutated using redux-immutable-state-invariant (only use in development!)
  3. Enforce with the use of a library such as immer, immutable.js or seamless-immutable.

Reducers

An action is eventually handled by a reducer. Metaphorically the reducer is the meat grinder of Redux, state goes in, state comes out.

function myReducer(state, action) {
  switch (action.type) {
    case "INCREMENT_COUNTER":
      //state.counter++; //BAD: never mutate
      //return state;
      return { ...state, counter: state.counter + 1 };
    default:
      return state;
  }
}

If and when the state is returned by a reducer, the store is updated.

Reducers must be pure. In other words a reducer must:

  • Never mutate state
  • Perform side effects, such as API calls or routing transitions
  • Call non-pure functions (e.g. Date.now, Math.random())

Therefore for a given input, and reducer is guaranted to alway return the same output.

When a dispatch is submitted, ALL reducers are invoked. That’s why its important for the untouched state to be returned as the default case of the switch statement.

A reducer should be independent and responsible for updates to a slice of state, however there are no hard and fast rules.

Each action can be handled by one or more reducers.

Each reducer can handle multiple actions.

Any React components that a glued up to the store are automatically updated, via a push notification using React-Redux.

React-Redux

Redux isn’t exclusive to React. With React-Redux, can tie React components to state in the Redux store.

Two problems it solves:

  • Attaching the app to the redux store (using a Provider component)
  • Creating container components (using the Connect function)

React-Redux Provider

Wrapping the root App component in a provider opens up the Redux store to every component in your application.

<Provider store={this.props.store}>
  <App />
</Provider>

React-Redux Connect

Before a container component is exported in the usual fashion, to connect it to the Redux store, it is wrapped with the connect function like so:

function mapStateToProps(state, ownProps) {
  return { authors: state.authors };
}

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

mapStateToProps

mapStateToProps defines what state needs to be exposed as props.

It returns an object that defines the data of interest. Each property defined on the object magically becomes a prop on React component. Anytime this data changes in the store, Redux will automatically fire mapStateToProps.

The more specific you can be in mapStateToProps is a performance win, as it will cut down on the number of possible state change notifications Redux needs to manage.

Important everytime the component is updated, the mapStateToProps function is called. Eeek. If there is some heavy lifting going on, such as transforming or sorting a large data structure, consider memoization (like caching for functions), where if the exact same state is presented as previously done, you can simply re-use the previously calculated results.

Memoizing libraries, called selectors, like reselect exist to make this fun.

What do selectors bring to the table?

  • They are efficient. A selector is not recomputed unless one of its arguments changes.
  • The can compute derived data, allowing Redux to store the minimal possible state.
  • They are composable. They can be used as input to other selectors.

Here’s one in action:

const getAllCoursesSelector = (state) => state.courses;

export const getCoursesSorted = createSelector(
  getAllCoursesSelector,
  (courses) => {
    return [...courses].sort((a, b) =>
      a.title.localeCompare(b.title, "en", { sensitivity: "base" })
    );
  }
);

mapDispatchToProps

How you expose redux actions to your components.

mapDispatchToProps defines what actions do I want on props. It receives dispatch as its lone parameter, and returns the callback props you want to pass down.

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(actions, dispatch),
  };
}

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

Four ways to deal with mapDispatchToProps:

Option 1: Simply ignore it (i.e. dont declare it in the call to connect), and use the implicitly created props.dispatch function directly in your component.

this.props.dispatch(loadProducts());

Option 2: Manually wrap calls to dispatch:

function mapDispatchToProps(dispatch) {
  return {
    loadProducts: () => {
      dispatch(loadProducts));
    },
    createProduct: (product) => {
      dispatch(createProduct(product));
    }
  }
}

To consume an action in the component becomes quite clean:

this.props.loadProducts();

Option 3: Use bindActionCreators

This ships with redux, and takes an array of actions. It will wrap each of them in a call to dispatch for you.

import * as courseActions from "../../redux/actions/courseActions";
...
...
function mapDispatchToProps(dispatch) {
  return {
    //this will shove all action creator functions into props
    //this.props.actions.loadCourses()
    actions: bindActionCreators(courseActions, dispatch),
  };
}

Option 4: Return objects

Declare mapDispatchToProps as an object, as opposed to a map. Redux connect will automatically wrap each action creator in dispatch.

const mapDispatchToProps = {
  incrementCounter,
};

Examples of mapDispatchToProps in action:

handleSubmit = (event) => {
  event.preventDefault(); //no postbacks
  // console.log(this.state.course.title);
  // debugger;

  // option 1: implicit dispatch
  // this.props.dispatch(courseActions.createCourse(this.state.course));

  // option 2: simple CRUD wrappers
  // this.props.createCourse(this.state.course);

  // option 3: bindActionCreators
  this.props.actions.createCourse(this.state.course);
};

Redux Setup

Initial setup is full on:

  1. Create action
  2. Create reducer
  3. Create root reducer
  4. Configure store
  5. Instantiate store
  6. Connect component
  7. Pass props via connect
  8. Dispatch action

But once the foundation is setup, adding features becomes much easier:

  1. Create action type constant (actionTypes.js)
  2. Create an action (new file in /redux/actions such as authorActions.js)
  3. Create (or enhance) reducer (new file in /redux/reducers such as authorReducer.js)
  4. Update root reducer (in index.js) to include new child reducer.
  5. Connect component
  6. Dispatch action

Async and APIs

Mock API

Often state will come from another source, like the server. It’s a good idea to kick off with a mock API. Why:

  • can get started immediately with getting bogged down in backend
  • is resilient to backend development instabilities
  • simulate and test high latency (i.e. slowness)
  • testing
  • seamless bind to the real API by just tweaking the imports at the top of thunks/sagas, or checking an environment variable to toggle between mock and real API.

For a simple mock API, checkout the top level tools directory:

  • apiServer.js a node mock API build using express and json-server, that reads in data from db.json
  • mockData.js javascript data structures, exported in commonjs format (for node)
  • createMockDb.js writes mock data to a file db.json

json-server is COOL. Not only do you get a nice frontend over the API, it supports all HTTP verbs, so you can PUT, POST, DELETE, GET and so on for free. It will maintain db.json based on the operations fired.

Wire these in as an npm script in package.json:

"scripts": {
  "start": "run-p start:dev start:api",
  "start:dev": "webpack-dev-server --config webpack.config.dev.js --port 3000",
  "prestart:api": "node tools/createMockDb.js",
  "start:api": "node tools/apiServer.js"
}

prestart scripts automatically fire before any start counterparts.

run-p will run a list of scripts in parallel, for example:

"scripts": {
  "start": "run-p start:dev start:api",
  "start:dev": "webpack-dev-server --config webpack.config.dev.js --port 3000",
  "prestart:api": "node tools/createMockDb.js",
  "start:api": "node tools/apiServer.js"
}

Run just the mock API with npm run start:api or both the API and frontend with npm run start

API Client Wrappers

Its tidy to centralise all API calls under an api folder. Each API should get its own wrapper, for example courseApi.js:

import { handleResponse, handleError } from "./apiUtils";
const baseUrl = process.env.API_URL + "/courses/";

export function getCourses() {
  return fetch(baseUrl).then(handleResponse).catch(handleError);
}

export function saveCourse(course) {
  return fetch(baseUrl + (course.id || ""), {
    method: course.id ? "PUT" : "POST", // POST for create, PUT to update when id already exists.
    headers: { "content-type": "application/json" },
    body: JSON.stringify(course),
  })
    .then(handleResponse)
    .catch(handleError);
}

export function deleteCourse(courseId) {
  return fetch(baseUrl + courseId, { method: "DELETE" })
    .then(handleResponse)
    .catch(handleError);
}

Note the vanilla REST API conventions of PUT for creates, POST for updates, DELETE for deletes and GET for retrieves.

Having these API wrappers in one place, give better control over environment matters such as the base URL.

const baseUrl = process.env.API_URL + "/courses/";

Webpack can inject this environment, by using the DefinePlugin in your webpack config:

plugins: [
  new webpack.DefinePlugin({
    "process.env.API_URL": JSON.stringify("http://localhost:3001"),
  }),

Redux Middleware

An extensibility option for redux.

+--------+     +------------+     +---------+
| Action +---->+ Middleware +---->+ Reducer |
+--------+     +------------+     +---------+

A convenient place to hook behaviour onto actions, such as:

  • Logging
  • Handling API calls
  • Crash reporting
  • Routing

Here’s an example logger. The signature chains blocks of middleware together, using a technique called currying.

const logger = store => next => action {
  console.group(action.type)
  console.info('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  console.groupEnd()
  return result
}

Reason to use middleware for async:

  1. Consistency, without middleware the signature of the dispatch calls would vary depending if they were synchronous or asynchronous.
  2. Purity, avoids binding code to side-effects.
  3. Testing, components free of side-effects are easier to test.g

Redux Async Libraries

  • redux-thunk by Dan Abramov (creator of redux) returns functions from action creator instead of objects.
  • redux-promise uses promises paired with flux standard actions
  • redux-observable dispatches RxJS observables
  • redux-saga a full blown async DSL based on ES6 generators

Thunks

Coined from compsci, a thunk is just a function that wraps a function to defer its evalutation.

In this case the call to dispatch is being deferred:

export function deleteVehicle(vehicleId) {
  return (dispatch, getState) => {
    return VehicleApi.deleteVehicle(vehicleId)
      .then(() => {
        dispatch(deletedVehicle(vehicleId));
      })
      .catch(handleError);
  };
}

redux-thunks get some built-in power:

  • Can access to the store.
  • Get dispatch injected automatically (hand crafting action creators manually would require manual wire up)
  • Passed getState, allows any conditional check against state (e.g. logged in user) before dispatching (i.e. a conditional dispatch)

The injection of dispatch is a win, as the function call retains parity with the sync version.

Register redux-thunk with the redux middleware when setting up the store:

import thunk from "redux-thunk";

export default function configureStore(initialState) {
  const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; //redux devtools

  return createStore(
    rootReducer,
    initialState,
    composeEnhancers(applyMiddleware(thunk, reduxImmutableStateInvariant()))
  );

Conditional mapStateToProps

In some scenarios the state that get bound to a components props, requires some additional logic. This is common when populating forms for creates versus updates. Setup props with empty new state for a new create, or for an update load the props up with existing data.

Before (sets the course to a new empty object every time):

function mapStateToProps(state) {
  return {
    course: newCourse,
    courses: state.courses,
    authors: state.authors,
  };
}

After:

export function getCourseBySlug(courses, slug) {
  return courses.find((course) => course.slug === slug) || null;
}

function mapStateToProps(state, ownProps) {
  const slug = ownProps.match.params.slug;
  const course =
    slug && state.courses.length > 0
      ? getCourseBySlug(state.courses, slug)
      : newCourse;

  return {
    course: course,
    courses: state.courses,
    authors: state.authors,
  };
}

mapStateToProps supports a second optional param called ownProps, which exposes the components own props, such as the various route data injected by react-router.

The getCourseBySlug is just filtering data, which is ultimately coming from the redux store. Functions like this become common in the app, and are known as selectors.

When make selectors (unlike this simple example above) consider:

  • placing this with reducers for greater reuse
  • memoising for perf with a lib like reselect

Polish (the finer things)

Spinner component

The top level component needs to track work in progress (WIP). A simple boolean prop is usually enough. If the prop.loading is true show the spinner, otherwise, render the normal markup.

render() {
  return (
    <>
      <h2>Courses</h2>
      {this.props.loading ? (
        <Spinner />
      ) : (
        <>
          <button ...

How you determine work in progress is specific to the app. One way to do this is by counting the number of API calls in flight.

function mapStateToProps(state, ownProps) {
  return {
    loading: state.apiCallsInProgress > 0,
  };
}

Redux actions that interact with API’s track this counter by in turn dispatching beginApiCall, endApiCall and apiCallError actions:

export function loadCourses() {
  return function (dispatch) {
    dispatch(beginApiCall());
    return courseApi
      .getCourses()
      .then((courses) => {
        dispatch(loadCoursesSuccess(courses));
      })
      .catch((error) => {
        dispatch(apiCallError());
        throw error;
      });
  };
}

Which as you expect, simply increments or decrements apiCallsInProgress:

export default function apiCallStatusReducer(
  state = initialState.apiCallsInProgress,
  action
) {
  if (action.type == types.BEGIN_API_CALL) {
    return state + 1;
  } else if (actionTypeEndsInSuccess(action.type)) {
    return state - 1;
  } else if (actionTypeEndsInError(action.type)) {
    return state - 1;
  }

  return state;
}

Status API and feedback

Disable UI elements while backend API’s are grinding away. Convey the outcome to the user with toasts.

For localised state, use of redux is overkill. In this component, a local piece of state called saving is used. setSaving(true) is set just before saveCourse is called:

function ManageCoursesPage({
  courses,
  authors,
  loadCourses,
  loadAuthors,
  saveCourse,
  history,
  ...props
}) {
  const [course, setCourse] = useState({ ...props.course });
  const [errors, setErrors] = useState({});
  const [saving, setSaving] = useState(false);

  function handleSave(event) {
    event.preventDefault();
    if (!formIsValid()) return;
    setSaving(true);
    saveCourse(course)
      .then(() => {
        toast.success("Course saved.");
        history.push("/courses");
      })
      .catch((error) => {
        setSaving(false);
        setErrors({ onSave: error.message });
      });
  }

This saving state can be propagated down to individual form components:

<CourseForm
  course={course}
  errors={errors}
  authors={authors}
  onChange={handleChange}
  onSave={handleSave}
  saving={saving}
/>

Which can disable inputs while a save is in progress:

const CourseForm = ({
  course,
  authors,
  onSave,
  onChange,
  saving = false,
  errors = {},
}) => {
  return (
    <form onSubmit={onSave}>
      <h2>{course.id ? "Edit" : "Add"} Course</h2>
      {errors.onSave && (
        <div className="alert alert-danger" role="alert">
          {errors.onSave}
        </div>
      )}
      <TextInput
        name="title"
        label="Title"
        value={course.title}
        onChange={onChange}
        error={errors.title}
      />

      <button type="submit" disabled={saving} className="btn btn-primary">
        {saving ? "Saving..." : "Save"}
      </button>
    </form>
  );
};

Server side validation

When interacting with an API, things can go wrong. The promise catch handler should capture the error, in the below example writes an object with a key of onSave to local state errors:

function handleSave(event) {
  event.preventDefault();
  if (!formIsValid()) return;
  setSaving(true);
  saveCourse(course)
    .then(() => {
      toast.success("Course saved.");
      history.push("/courses");
    })
    .catch((error) => {
      setSaving(false);
      setErrors({ onSave: error.message });
    });
}

The errors object is propagated to the downstream form component:

<CourseForm
  course={course}
  errors={errors}
  authors={authors}
  onChange={handleChange}
  onSave={handleSave}
  saving={saving}
/>

Which checks for and conditionally shows any errors.onSave:

const CourseForm = ({
  course,
  authors,
  onSave,
  onChange,
  saving = false,
  errors = {}
}) => {
  return (
    <form onSubmit={onSave}>
      <h2>{course.id ? "Edit" : "Add"} Course</h2>
      {errors.onSave && (
        <div className="alert alert-danger" role="alert">
          {errors.onSave}
        </div>
      )}

Client side validation

Store local (non redux) state on the component, that stores a dictionary of errors. Before writing the data in a form to an API, it should be validated (as per formIsValid below):

function ManageCoursesPage({
  courses,
  authors,
  loadCourses,
  loadAuthors,
  saveCourse,
  history,
  ...props
}) {
  const [course, setCourse] = useState({ ...props.course });
  const [errors, setErrors] = useState({});
  const [saving, setSaving] = useState(false);

  function formIsValid() {
    const { title, authorId, category } = course;
    const errors = {};

    if (!title) errors.title = "Title is required.";
    if (!authorId) errors.author = "Author is required.";
    if (!category) errors.category = "Category is required.";

    setErrors(errors);
    return Object.keys(errors).length === 0;
  }

  function handleSave(event) {
    event.preventDefault();
    if (!formIsValid()) return;
    setSaving(true);
    saveCourse(course)
      .then(() => {
        toast.success("Course saved.");
        history.push("/courses");
      })
      .catch((error) => {
        setSaving(false);
        setErrors({ onSave: error.message });
      });
  }

In course form, this object of errors can be passed down. Individual form element can check for the presence of an error that relates to their data (e.g. title or author), and flag to the user that individual inputs are invalid:

const CourseForm = ({
  course,
  authors,
  onSave,
  onChange,
  saving = false,
  errors = {}
}) => {
  return (
    <form onSubmit={onSave}>
      <h2>{course.id ? "Edit" : "Add"} Course</h2>
      {errors.onSave && (
        <div className="alert alert-danger" role="alert">
          {errors.onSave}
        </div>
      )}
      <TextInput
        name="title"
        label="Title"
        value={course.title}
        onChange={onChange}
        error={errors.title}
      />

      <SelectInput
        name="authorId"
        label="Author"
        value={course.authorId || ""}
        defaultOption="Select Author"
        options={authors.map(author => ({
          value: author.id,
          text: author.name
        }))}
        onChange={onChange}
        error={errors.author}
      />

      <TextInput
        name="category"
        label="Category"
        value={course.category}
        onChange={onChange}
        error={errors.category}
      />

The low level UI components such as TextInput show a red validation error if applicable:

const TextInput = ({ name, label, onChange, placeholder, value, error }) => {
  let wrapperClass = "form-group";
  if (error && error.length > 0) {
    wrapperClass += " " + "has-error";
  }

  return (
    <div className={wrapperClass}>
      <label htmlFor={name}>{label}</label>
      <div className="field">
        <input
          type="text"
          name={name}
          className="form-control"
          placeholder={placeholder}
          value={value}
          onChange={onChange}
        />
        {error && <div className="alert alert-danger">{error}</div>}
      </div>
    </div>
  );
};

Optimistic deletes

Keep the UI upto date, regardless of the outcome of backend API’s, giving the user a snappy experience.

In the component, can fire the action as soon as the user triggers an event (such as deleting a course):

handleDeleteCourse = (course) => {
  toast.success("Course deleted.");
  this.props.actions.deleteCourse(course).catch((error) => {
    toast.error("Delete failed." + error.message, { autoClose: false });
  });
};

The redux action, updates the state (removes a course from the store), and calls the API. Removing the course from the redux store does not hinge on the outcome of the API call:

export function deleteCourse(course) {
  return function (dispatch, getState) {
    dispatch(deleteCourseOptimistic(course));
    return courseApi.deleteCourse(course.id);
  };
}

export function deleteCourseOptimistic(course) {
  return { type: types.DELETE_COURSE_OPTIMISTIC, course };
}

The reducer does its immutable thing (strips out the course in this case - filter returns a new copy of the original array it iterates over, meeting the immutable requirement of redux):

export default function courseReducer(state = initialState.courses, action) {
  switch (action.type) {
    case types.DELETE_COURSE_OPTIMISTIC:
      return state.filter((course) => course.id !== action.course.id);
    default:
      return state;
  }
}

Testing

Redux Connected Components

When a container component is wired to redux, it is wrapped in a call to connect right?

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

To test, either wrap the component in <Provider> in the unit test, or add named export for unconnected version component.

I prefer the first option, as it is more explicit than relying on default exports in the non-test code in the app.

import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";
import { mount } from "enzyme";
import { authors, newCourse, courses } from "../../../tools/mockData";
import ManageCoursePage from "./ManageCoursePage";
import rootReducer from "../../redux/reducers";

function render(args) {
  const defaultProps = {
    authors,
    courses,
    history: {}, //router
    saveCourse: jest.fn(),
    loadAuthors: jest.fn(),
    loadCourses: jest.fn(),
    course: newCourse,
    match: { params: {} }, //router
  };

  const props = { ...defaultProps, ...args };

  const store = createStore(rootReducer, {
    courses: courses,
    authors: authors,
  });

  return mount(
    <Provider store={store}>
      <ManageCoursePage {...props} />
    </Provider>
  );
}

it("sets error when attempting to save an empty title field", () => {
  const wrapper = render();
  wrapper.find("form").simulate("submit");
  const error = wrapper.find(".alert").first();
  expect(error.text()).toBe("Title is required.");
});

Note how the store needs to be primed with mock data.

Action Creators

import * as courseActions from "./courseActions";
import * as types from "./actionTypes";
import { courses } from "../../../tools/mockData";

// Test an async action
const middleware = [thunk];
const mockStore = configureMockStore(middleware);

describe("createCourseSuccess", () => {
  it("should create a CREATE_COURSE_SUCCESS action", () => {
    //arrange
    const course = courses[0];
    const expectedAction = {
      type: types.CREATE_COURSE_SUCCESS,
      course,
    };

    //act
    const action = courseActions.createCourseSuccess(course);

    //assert
    expect(action).toEqual(expectedAction);
  });
});

Thunks

import * as courseActions from "./courseActions";
import * as types from "./actionTypes";
import { courses } from "../../../tools/mockData";
import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import configureMockStore from "redux-mock-store";

const middleware = [thunk];
const mockStore = configureMockStore(middleware);

describe("Async actions", () => {
  afterEach(() => {
    fetchMock.restore();
  });

  describe("Load courses thunk", () => {
    it("should create BEGIN_API_CALL and LOAD_COURSES_SUCCESS when loading courses", () => {
      fetchMock.mock("*", {
        body: courses,
        headers: { "content-type": "application/json" },
      });

      const expectedActions = [
        { type: types.BEGIN_API_CALL },
        { type: types.LOAD_COURSES_SUCCESS, courses },
      ];

      const store = mockStore({ courses: [] });
      return store.dispatch(courseActions.loadCourses()).then(() => {
        expect(store.getActions()).toEqual(expectedActions);
      });
    });
  });
});

Reducers

import courseReducer from "./courseReducer";
import * as actions from "../actions/courseActions";

it("should add course when passed CREATE_COURSE_SUCCESS", () => {
  // arrange
  const initialState = [
    {
      title: "A",
    },
    {
      title: "B",
    },
  ];

  const newCourse = {
    title: "C",
  };

  const action = actions.createCourseSuccess(newCourse);

  // act
  const newState = courseReducer(initialState, action);

  // assert
  expect(newState.length).toEqual(3);
  expect(newState[0].title).toEqual("A");
  expect(newState[1].title).toEqual("B");
  expect(newState[2].title).toEqual("C");
});

Store

import { createStore } from "redux";
import rootReducer from "./reducers";
import initialState from "./reducers/initialState";
import * as courseActions from "./actions/courseActions";

it("Should handle creating courses", function () {
  // arrange
  const store = createStore(rootReducer, initialState);
  const course = {
    title: "Clean Code",
  };

  // act
  const action = courseActions.createCourseSuccess(course);
  store.dispatch(action);

  // assert
  const createdCourse = store.getState().courses[0];
  expect(createdCourse).toEqual(course);
});