SaltyCrane Blog — Notes on JavaScript and web development

Caching a filtered list of results w/ Redux, React Router, and redux-promise-memo

This post shows how to cache API data for a React + Redux application using ideas from my library, redux-promise-memo. The example app displays a filtered list of vehicles, a sidebar with make and model filters, and a detail page for each vehicle. Caching is used to prevent re-fetching API data when navigating between the detail pages and the list page.

In addition to React and Redux, this example uses React Router and redux-promise-middleware, though alternatives like redux-pack or Gluestick's promise middleware can be used also.

The example is broken into 3 sections: 1. Basic features (no caching), 2. Caching (manual setup), and 3. Caching with redux-promise-memo.

Basic features
  • filtering vehicles by make and model is done by the backend vehicles API
  • when a make is selected, the models API populates the models filter for the selected make
  • filter parameters (make and model) are stored in the URL query string to support deep linking to a page of filtered results and to support browser "back" and "forward" navigation
  • each vehicle detail page also has a unique route using the vehicle id in the route
Highlighted feature - caching
  • API responses are not re-fetched when moving back and forward between pages

Code for basic features (no caching)

The full example code is here on github. A demo is deployed here.

VehicleFilters.js
class VehiclesFilters extends React.Component {
  componentDidMount() {
    this._fetchData();
  }

  componentDidUpdate(prevProps) {
    if (this.props.query !== prevProps.query) {
      this._fetchData();
    }
  }

  _fetchData() {
    let { dispatch, query } = this.props;
    dispatch(actions.fetchModels(query.make));
  }

  render() {
    let { changeQuery, models, query } = this.props;
    return (
      <Container>
        <Select
          label="Make"
          onChange={e =>
            changeQuery({ make: e.currentTarget.value, model: "" })
          }
          options={["All makes", "Acura", "BMW", "Cadillac", "..."]}
          value={query.make || ""}
        />
        {query.make && (
          <Select
            label="Model"
            onChange={e => changeQuery({ model: e.currentTarget.value })}
            options={["All models", ...models]}
            value={query.model || ""}
          />
        )}
      </Container>
    );
  }
}

export default compose(
  withVehiclesRouter,
  connect(state => ({ models: state.models }))
)(VehiclesFilters);

The VehicleFilters component has <select> inputs for the "Make" and "Model" filters.

  1. when a user changes the make to "BMW", the changeQuery function adds the ?make=BMW query string to the URL
  2. when the query string is updated, componentDidUpdate calls fetchModels which calls the models API
  3. when the API responds, the models list is stored in Redux at state.models
  4. when the Redux state changes, the "Model" <select> is updated with the new list of models

Notes:

  • withVehiclesRouter is a higher-order component that adds the following props to VehicleFilters:

    • query - the parsed query string
    • changeQuery - a function used to update the query string

    See the implementation here

  • the route is the single source of truth for the make and model parameters. They are stored only in the route and not in Redux.

VehiclesList.js
class VehiclesList extends React.Component {
  componentDidMount() {
    this._fetchData();
  }

  componentDidUpdate(prevProps) {
    if (this.props.query !== prevProps.query) {
      this._fetchData();
    }
  }

  _fetchData() {
    let { dispatch, query } = this.props;
    dispatch(actions.fetchVehicles(query));
  }

  render() {
    let { isLoading, vehicles } = this.props;
    return (
      <Container>
        {isLoading ? (
          <Spinner />
        ) : (
          vehicles.map(vehicle => (
            <Link key={vehicle.id} to={`/vehicles/${vehicle.id}`}>
              <VehicleCard {...vehicle} />
            </Link>
          ))
        )}
      </Container>
    );
  }
}

export default compose(
  withVehiclesRouter,
  connect(state => ({
    isLoading: state.isLoading,
    vehicles: state.vehicles
  }))
)(VehiclesList);

The VehiclesList component gets data the same way the "Model" <select> input does.

  1. the previous VehicleFilters component updates the route query string with a make or model
  2. when the route query string is updated, this component's componentDidUpdate calls fetchVehicles which calls the vehicles API
  3. when the API responds, the vehicle list is stored in Redux at state.vehicles
  4. when the Redux state changes, this component is updated with the new list of vehicles.

Each vehicle card is wrapped with a react-router <Link>. Clicking the vehicle card navigates to a new route, /vehicles/{vehicleId}.

VehicleDetail.js
class VehicleDetail extends React.Component {
  componentDidMount() {
    let { dispatch, vehicleId } = this.props;
    dispatch(actions.fetchVehicle(vehicleId));
  }

  render() {
    let { isLoading, vehicle } = this.props;
    return isLoading ? <Spinner /> : <VehicleCard {...vehicle} />;
  }
}

export default compose(
  withVehiclesRouter,
  connect(state => ({
    isLoading: state.isLoading,
    vehicle: state.vehicle
  }))
)(VehicleDetail);

VehicleDetail gets data in the same way as the "Model" filter and VehiclesList. One difference is that it doesn't need to use componentDidUpdate because the API input parameter (vehicleId) never changes.

  1. to display a vehicle detail page, a vehicle <Link> is clicked in the previous VehiclesList component which changes the route to /vehicles/{vehicleId}
  2. when the route changes, this component is rendered and passed the vehicleId prop.
  3. when this component is rendered, componentDidMount calls fetchVehicle which calls the vehicle detail API

The withVehiclesRouter higher-order component takes match.params.vehicleId from react-router and passes it to VehicleDetail as vehicleId.

App.js
let store = createStore(reducer, applyMiddleware(promiseMiddleware()));

let VehiclesPage = () => (
  <React.Fragment>
    <VehiclesFilters />
    <VehiclesList />
  </React.Fragment>
);

let App = () => (
  <Provider store={store}>
    <BrowserRouter>
      <Switch>
        <Route component={VehicleDetail} path="/vehicles/:vehicleId" />
        <Route component={VehiclesPage} path="/vehicles" />
      </Switch>
    </BrowserRouter>
  </Provider>
);

This shows react-router route configuration for the app and also the addition of redux-promise-middleware.

actions.js
export let fetchModels = make => ({
  type: "FETCH_MODELS",
  payload: fakeModelsApi(make)
});

export let fetchVehicle = vehicleId => ({
  type: "FETCH_VEHICLE",
  payload: fakeVehicleApi(vehicleId)
});

export let fetchVehicles = params => ({
  type: "FETCH_VEHICLES",
  payload: fakeVehiclesApi(params)
});
reducers.js
let isLoading = (state = false, action) => {
  switch (action.type) {
    case "FETCH_VEHICLE_PENDING":
    case "FETCH_VEHICLES_PENDING":
      return true;
    case "FETCH_VEHICLE_FULFILLED":
    case "FETCH_VEHICLES_FULFILLED":
      return false;
    default:
      return state;
  }
};

let models = (state = [], action) => {
  switch (action.type) {
    case "FETCH_MODELS_FULFILLED":
      return action.payload;
    default:
      return state;
  }
};

let vehicle = (state = [], action) => {
  switch (action.type) {
    case "FETCH_VEHICLE_FULFILLED":
      return action.payload;
    default:
      return state;
  }
};

let vehicles = (state = [], action) => {
  switch (action.type) {
    case "FETCH_VEHICLES_FULFILLED":
      return action.payload;
    default:
      return state;
  }
};

export default combineReducers({
  isLoading,
  models,
  vehicle,
  vehicles
});

These are the actions and reducers that are using redux-promise-middleware.

  • fakeModelsApi, fakeVehicleApi, and fakeVehiclesApi are meant to mimick a HTTP client like fetch or axios. They return a promise that resolves with some canned data after a 1 second delay.
  • the models, vehicle, and vehicles reducers store the API responses in the Redux state

Caching (manual setup)

In the above setup, API calls are made unecessarily when navigating back and forth between vehicle details pages and the main vehicles list. The goal is to eliminate the uncessary calls.

The vehicle data is already "cached" in Redux. But the app needs to be smarter about when to fetch new data. In some apps, a component can check if API data exisits in Redux and skip the API call if it is present. In this case, however, doing this would not allow updating the results when the make or model filters change.

The approach I took in redux-promise-memo was to fetch only when API input parameters changed:

  • the API input parameters (e.g. make, model) are stored in Redux
  • when deciding whether to make a new API call, the current API input paramters are tested to see if they match the parameters previously stored in Redux
  • if they match, the API call is skipped, and the data already stored in Redux is used

Below are changes that can be made to implement this idea without using the library. The solution with the redux-promise-memo library is shown at the end.

The full example code is here on github. A demo is deployed here.

actions.js
export let fetchVehicles = params => ({
  type: "FETCH_VEHICLES",
  payload: fakeVehiclesApi(params),
  meta: { params } // <= NEW: add the API params to the action
});
reducers.js
// NEW: add this vehiclesCacheParams reducer
let vehiclesCacheParams = (state = null, action) => {
  switch (action.type) {
    case "FETCH_VEHICLES_FULFILLED":
      return action.meta.params;
    default:
      return state;
  }
};

The actions and reducers are updated to store the API parameters.

  • in the fetchVehicles action creator, the API params is added to the action
  • a new reducer, vehiclesCacheParams, is added which stores those params in Redux when the API succeeds
VehiclesList.js
class VehiclesList extends React.Component {
  _fetchData() {
    let { cacheParams, dispatch, query } = this.props;

    // NEW: add this "if" statement to check if API params have changed
    if (JSON.stringify(query) !== JSON.stringify(cacheParams)) {
      dispatch(actions.fetchVehicles(query));
    }
  }

  // ...the rest is the same as before
}

export default compose(
  withVehiclesRouter,
  connect(state => ({
    cacheParams: state.vehiclesCacheParams, // <= NEW line here
    isLoading: state.isLoading,
    vehicles: state.vehicles
  }))
)(VehiclesList);
  • in VehiclesList, the vehicleCacheParams state is added to the connect call
  • then an "if" condition is added around the fetchVehicles action dispatch. JSON.stringify is used to compare arguments that are objects or arrays.

Caching using redux-promise-memo

To avoid adding this boilerplate for every API, I abstracted the above idea into the redux-promise-memo library.

  • it uses a reducer to store the API input parameters like the manual solution
  • it provides a memoize function to wrap promise-based action creators (like those created with redux-promise-middleware above)
  • the memoize wrapper checks if the API input arguments have changed. If the input arguments match what is stored in Redux, the API call is skipped.
  • it also stores the "loading" and "success" state of the API. It skips the API call if the previous API call is loading or sucessful. But it re-fetches if there was an error.
  • it has an option to support multiple caches per API. If data is stored in a different places in Redux per set of input arguments, this option can be used.
  • it provides support for "invalidating" the cache using any Redux actions.
  • it provides support for other libraries besides redux-promise-middleware. Custom matchers for other libraries can be written. Examples of matchers are here

The full example code is here on github. A demo is deployed here.

Install redux-promise-memo

redux-promise-memo uses redux-thunk so it needs to be installed as well.

npm install redux-promise-memo redux-thunk
index.js
import thunk from "redux-thunk";

let store = createStore(reducer, applyMiddleware(thunk, promiseMiddleware()));

Add redux-thunk to redux middleware

reducers.js
// NEW: remove the vehiclesCacheParams reducer

// NEW: add the _memo reducer
import {
  createMemoReducer,
  reduxPromiseMiddlewareConfig
} from "redux-promise-memo";

let _memo = createMemoReducer(reduxPromiseMiddlewareConfig);

let rootReducer = combineReducers({
  _memo,
  isLoading,
  models,
  vehicle,
  vehicles
});
  • create the redux-promise-memo reducer and add it to the root reducer. IMPORTANT: the Redux state slice must be named _memo for it to work. I tried to create a Redux enhancer to handle this automatically, but did not get it to work reliably with other libraries.
  • remove the *CacheParams reducers to store API input params from our manual solution
actions.js
import { memoize } from "redux-promise-memo";

let _fetchModels = make => ({
  type: "FETCH_MODELS",
  payload: fakeModelsApi(make)
  // NEW: remove the API params from the action
});
// NEW: wrap the action creator with `memoize`
export let memoizedFetchModels = memoize(_fetchModels, "FETCH_MODELS");

let _fetchVehicle = vehicleId => ({
  type: "FETCH_VEHICLE",
  payload: fakeVehicleApi(vehicleId)
});
export let memoizedFetchVehicle = memoize(_fetchVehicle, "FETCH_VEHICLE");

let _fetchVehicles = params => ({
  type: "FETCH_VEHICLES",
  payload: fakeVehiclesApi(params)
});
export let memoizedFetchVehicles = memoize(_fetchVehicles, "FETCH_VEHICLES");
  • wrap the action creators with the memoize higher order function
  • specify a "key" to be used to separate parameters in the reducer. The action type is recommended to be used as the key.
VehiclesList.js
class VehiclesList extends React.Component {
  componentDidUpdate(prevProps) {
    // NEW: removed "if" condition here
    this._fetchData();
  }

  _fetchData() {
    let { dispatch, query } = this.props;
    // NEW: removed "if" condition here
    dispatch(actions.memoizedFetchVehicles(query));
  }

  // ...the rest is the same as before
}

export default compose(
  withVehiclesRouter,
  connect(state => ({
    // NEW: removed the cacheParams line here
    isLoading: state.isLoading,
    vehicles: state.vehicles
  }))
)(VehiclesList);
  • update components to remove "if" conditions because the library does the check
  • remove the use of state.vehicleCacheParams

This solution should behave similarly to the manual solution with less boilerplate code. In the development environment only, it also logs console messages showing if the API is requesting, loading, or cached.

What does Redux's combineReducers do?

Redux uses a single root reducer function that accepts the current state (and an action) as input and returns a new state. Users of Redux may write the root reducer function in many different ways, but a recommended common practice is breaking up the state object into slices and using a separate sub reducer to operate on each slice of the state. Usually, Redux's helper utility, combineReducers is used to do this. combineReducers is a nice shortcut because it encourages the good practice of reducer composition, but the abstraction can prevent understanding the simplicity of Redux reducers.

The example below shows how a root reducer could be written without combineReducers:

Given a couple of reducers:

function apples(state, action) {
  // do stuff
  return state;
};

function bananas(state, action) {
  // do stuff
  return state;
};

This reducer created with combineReducers:

const rootReducer = combineReducers({ apples, bananas });

is equivalent to this reducer:

function rootReducer(state = {}, action) {
  return {
    apples: apples(state.apples, action),
    bananas: bananas(state.bananas, action),
  };
};

Usage without ES6 concise properties

The above example used ES6 concise properties, but combineReducers can also be used without concise properties. This reducer created with combineReducers:

const rootReducer = combineReducers({
  a: apples,
  b: bananas
});

is equivalent to this reducer:

function rootReducer(state = {}, action) {
  return {
    a: apples(state.a, action),
    b: bananas(state.b, action),
  };
};

Understanding how combineReducers works can be helpful in learning other ways reducers can be used.

References / see also

How to set up a React Apollo client with a Graphcool GraphQL server

Graphcool is a service similar to Firebase except it is used to create GraphQL APIs instead of RESTful ones. Apollo Client is a GraphQL client (alternative to Relay) that can be used with React (and other frontend frameworks). Below is how to create a Graphcool GraphQL service and query it from a React frontend using Apollo Client. I also used Next.js to set up the React project. I am running Node 8.4.0 on macOS Sierra. The code for this example is on github: https://github.com/saltycrane/graphcool-apollo-example.

Jump to: Graphcool setup, Next.js setup, or Apollo setup.

Graphcool GraphQL server

Graphcool Apollo Quickstart: https://www.graph.cool/docs/quickstart/frontend/react/apollo-tijghei9go

  • Install the Graphcool command-line tool (step 2 in quickstart)
    $ npm install -g graphcool-framework 
    
    $ graphcool-framework --version
    graphcool-framework/0.11.5 (darwin-x64) node-v8.4.0 
    
  • Create a Graphcool service (step 3 in quickstart)
    $ cd /tmp
    $ mkdir graphcool-apollo-example
    $ cd graphcool-apollo-example 
    
    $ graphcool-framework init myserver
    Creating a new Graphcool service in myserver... ✔
    
    Written files:
    ├─ types.graphql
    ├─ src
    │  ├─ hello.js
    │  └─ hello.graphql
    ├─ graphcool.yml
    └─ package.json
    
    To get started, cd into the new directory:
      cd myserver
    
    To deploy your Graphcool service:
      graphcool deploy
    
    To start your local Graphcool cluster:
      graphcool local up
    
    To add facebook authentication to your service:
      graphcool add-template auth/facebook
    
    You can find further instructions in the graphcool.yml file,
    which is the central project configuration.
    
  • Add a Post type definition. (step 4 in quickstart) Edit myserver/types.graphql:
    type Post @model {
      id: ID! @isUnique    # read-only (managed by Graphcool)
      createdAt: DateTime! # read-only (managed by Graphcool)
      updatedAt: DateTime! # read-only (managed by Graphcool)
    
      description: String!
      imageUrl: String!
    }
  • Deploy the Graphcool server (step 5 in quickstart). After running the deploy command, it will ask to select a cluster, select a name, and create an account with Graphcool.
    $ cd myserver
    $ graphcool-framework deploy
    ? Please choose the cluster you want to deploy to
    
    shared-eu-west-1
    
    Auth URL: https://console.graph.cool/cli/auth?cliToken=xxxxxxxxxxxxxxxxxxxxxxxxx&authTrigger;=auth
    Authenticating... ✔
    
    Creating service myserver in cluster shared-eu-west-1... ✔
    Bundling functions... 2.3s
    Deploying... 1.3s
    
    Success! Created the following service:
    
    Types
    
      Post
       + A new type with the name `Post` is created.
       ├─ +  A new field with the name `createdAt` and type `DateTime!` is created.
       ├─ +  A new field with the name `updatedAt` and type `DateTime!` is created.
       ├─ +  A new field with the name `description` and type `String!` is created.
       └─ +  A new field with the name `imageUrl` and type `String!` is created.
    
    Resolver Functions
    
      hello
       + A new resolver function with the name `hello` is created.
    
    Permissions
    
      Wildcard Permission
       ? The wildcard permission for all operations is added.
    
    Here are your GraphQL Endpoints:
    
      Simple API:        https://api.graph.cool/simple/v1/cjc2uk4kx0vzo01603rkov391
      Relay API:         https://api.graph.cool/relay/v1/cjc2uk4kx0vzo01603rkov391
      Subscriptions API: wss://subscriptions.graph.cool/v1/cjc2uk4kx0vzo01603rkov391 
    
  • Run some queries in the Graphcool playground (step 6 in quickstart). Run the following command to open a new browser tab with the Graphcool playground:
    $ graphcool-framework playground 
    
    Run a query to create a post:
    mutation {
      createPost(
        description: "A rare look into the Graphcool office"
        imageUrl: "https://media2.giphy.com/media/xGWD6oKGmkp6E/200_s.gif"
      ) {
        id
      }
    }
    Run a query to retrieve all posts:
    query {
      allPosts {
        id
        description
        imageUrl
      }
    }
  • Done with Graphcool server. Also see https://console.graph.cool/server/schema/types

Next.js React frontend

Set up a React frontend using the Next.js framework. Next.js setup: https://github.com/zeit/next.js#setup

  • Create a myclient directory alongside the myserver directory:
    $ cd /tmp/graphcool-apollo-example
    $ mkdir myclient
    $ cd myclient 
    
  • Create myclient/package.json:
    {
      "dependencies": {
        "next": "4.2.1",
        "react": "16.2.0",
        "react-dom": "16.2.0"
      },
      "scripts": {
        "dev": "next",
        "build": "next build",
        "start": "next start"
      }
    }
  • Install Next.js and React:
    $ npm install
    npm WARN deprecated npmconf@2.1.2: this package has been reintegrated into npm and is now out of date with respect to npm
    npm WARN deprecated @semantic-release/last-release-npm@2.0.2: Use @semantic-release/npm instead
    
    > fsevents@1.1.3 install /private/tmp/graphcool-apollo-example/myclient/node_modules/fsevents
    > node install
    
    [fsevents] Success: "/private/tmp/graphcool-apollo-example/myclient/node_modules/fsevents/lib/binding/Release/node-v57-darwin-x64/fse.node" already installed
    Pass --update-binary to reinstall or --build-from-source to recompile
    
    > uglifyjs-webpack-plugin@0.4.6 postinstall /private/tmp/graphcool-apollo-example/myclient/node_modules/uglifyjs-webpack-plugin
    > node lib/post_install.js
    
    npm notice created a lockfile as package-lock.json. You should commit this file.
    npm WARN myclient No description
    npm WARN myclient No repository field.
    npm WARN myclient No license field.
    
    + react-dom@16.2.0
    + react@16.2.0
    + next@4.2.1
    added 838 packages in 20.012s
    
  • Create a Hello World page
    $ mkdir pages
    
    Create myclient/pages/index.js:
    const Home = () => <div>Hello World</div>;
    
    export default Home;
    
  • Run the Next.js dev server
    $ npm run dev 
    
    Go to http://localhost:3000 in the browser

Apollo Client

Set up Apollo Client to query the Graphcool server. Apollo Client setup: https://www.apollographql.com/docs/react/basics/setup.html

  • Install Apollo Client
    $ cd /tmp/graphcool-apollo-example/myclient 
    
    $ npm install apollo-client-preset react-apollo graphql-tag graphql 
    npm WARN apollo-link-http@1.3.2 requires a peer of graphql@^0.11.0 but none is installed. You must install peer dependencies yourself.
    npm WARN myclient No description
    npm WARN myclient No repository field.
    npm WARN myclient No license field.
    
    + react-apollo@2.0.4
    + graphql-tag@2.6.1
    + apollo-client-preset@1.0.6
    + graphql@0.12.3
    added 20 packages in 5.476s
    
  • Set up Apollo Client. Edit myclient/pages/index.js:
    import { InMemoryCache } from "apollo-cache-inmemory";
    import { ApolloClient } from "apollo-client";
    import { HttpLink } from "apollo-link-http";
    import { ApolloProvider } from "react-apollo";
    
    const client = new ApolloClient({
      link: new HttpLink({
        // Replace this with your Graphcool server URL
        uri: "https://api.graph.cool/simple/v1/cjc2uk4kx0vzo01603rkov391",
      }),
      cache: new InMemoryCache(),
    });
    
    const Home = () => <div>Hello World</div>;
    
    const App = () => (
      <ApolloProvider client={client}>
        <Home />
      </ApolloProvider>
    );
    
    export default App;
    
  • Try running the dev server
    $ npm run dev 
    
    Go to http://localhost:3000 in the browser
  • But, get this error:
    Error: fetch is not found globally and no fetcher passed, to fix pass a fetch for
          your environment like https://www.npmjs.com/package/node-fetch.
    
          For example:
            import fetch from 'node-fetch';
            import { createHttpLink } from 'apollo-link-http';
    
            const link = createHttpLink({ uri: '/graphql', fetch: fetch });
    
        at warnIfNoFetch (/private/tmp/graphcool-apollo-example/myclient/node_modules/apollo-link-http/lib/httpLink.js:72:15)
        at createHttpLink (/private/tmp/graphcool-apollo-example/myclient/node_modules/apollo-link-http/lib/httpLink.js:89:5)
        at new HttpLink (/private/tmp/graphcool-apollo-example/myclient/node_modules/apollo-link-http/lib/httpLink.js:159:34)
        at Object. (/private/tmp/graphcool-apollo-example/myclient/.next/dist/pages/index.js:25:9)
          at Module._compile (module.js:573:30)
          at Module._compile (/private/tmp/graphcool-apollo-example/myclient/node_modules/source-map-support/source-map-support.js:492:25)
          at Object.Module._extensions..js (module.js:584:10)
          at Module.load (module.js:507:32)
          at tryModuleLoad (module.js:470:12)
          at Function.Module._load (module.js:462:3)
  • Install node-fetch. This is needed because Next.js runs on a Node server in addition to the browser.
    $ npm install node-fetch
    npm WARN apollo-link-http@1.3.2 requires a peer of graphql@^0.11.0 but none is installed. You must install peer dependencies yourself.
    npm WARN myclient No description
    npm WARN myclient No repository field.
    npm WARN myclient No license field.
    
    + node-fetch@1.7.3
    updated 1 package in 4.028s
    
  • Update the code to use node-fetch as described in the error message:
    import { InMemoryCache } from "apollo-cache-inmemory";
    import { ApolloClient } from "apollo-client";
    import { createHttpLink } from "apollo-link-http";
    import gql from "graphql-tag";
    import fetch from "node-fetch";
    import { ApolloProvider } from "react-apollo";
    
    const client = new ApolloClient({
      link: createHttpLink({
        // Replace this with your Graphcool server URL
        uri: "https://api.graph.cool/simple/v1/cjc2uk4kx0vzo01603rkov391",
        fetch: fetch,
      }),
      cache: new InMemoryCache(),
    });
    
    class Home extends React.Component {
      componentDidMount() {
        client
          .query({
            query: gql`
              {
                allPosts {
                  id
                  description
                  imageUrl
                }
              }
            `,
          })
          .then(console.log);
      }
    
      render() {
        return <div>Look in the devtools console</div>;
      }
    }
    
    const App = () => (
      <ApolloProvider client={client}>
        <Home />
      </ApolloProvider>
    );
    
    export default App;
    
  • Try running the dev server again
    $ npm run dev 
    
    Go to http://localhost:3000 in the browser
  • It works. Open the browser devtools console and see the result of the query:
    {
      "data": {
        "allPosts": [
          {
            "id": "cjbffxjq5rrvd0192qmptpm2f",
            "description": "A rare look into the Graphcool office",
            "imageUrl": "https://media2.giphy.com/media/xGWD6oKGmkp6E/200_s.gif",
            "__typename": "Post"
          }
        ]
      },
      "loading": false,
      "networkStatus": 7,
      "stale": false
    }
  • Use the graphql higher-order component to make things nicer. Edit myclient/pages/index.js:
    import { InMemoryCache } from "apollo-cache-inmemory";
    import { ApolloClient } from "apollo-client";
    import { createHttpLink } from "apollo-link-http";
    import gql from "graphql-tag";
    import fetch from "node-fetch";
    import { ApolloProvider, graphql } from "react-apollo";
    
    const client = new ApolloClient({
      link: createHttpLink({
        // Replace this with your Graphcool server URL
        uri: "https://api.graph.cool/simple/v1/cjc2uk4kx0vzo01603rkov391",
        fetch: fetch,
      }),
      cache: new InMemoryCache(),
    });
    
    const MY_QUERY = gql`
      {
        allPosts {
          id
          description
          imageUrl
        }
      }
    `;
    
    const Home = ({ data }) => <pre>{JSON.stringify(data, null, 2)}</pre>;
    
    const HomeWithData = graphql(MY_QUERY)(Home);
    
    const App = () => (
      <ApolloProvider client={client}>
        <HomeWithData />
      </ApolloProvider>
    );
    
    export default App;
    
  • Run the dev server
    $ npm run dev 
    
    Go to http://localhost:3000 in the browser and see this result on the page:
    {
      "variables": {},
      "loading": false,
      "networkStatus": 7,
      "allPosts": [
        {
          "id": "cjbffxjq5rrvd0192qmptpm2f",
          "description": "A rare look into the Graphcool office",
          "imageUrl": "https://media2.giphy.com/media/xGWD6oKGmkp6E/200_s.gif",
          "__typename": "Post"
        }
      ]
    }

Docker cheat sheet

An image is a read-only template with instructions for creating a Docker container.
A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.
A container is a runnable instance of an image. You can create, start, stop, move, or delete a container. A container is a process which runs on a host. ...the container process that runs is isolated in that it has its own file system, its own networking, and its own isolated process tree separate from the host.

Listing

Removing

Pulling images

Publishing images

See also: Get started with Docker - Share you image

Building images from Dockerfiles

Creating containers

Starting / stopping containers

Running containers

docker run is a combination of (optionally) docker pull, docker create, and docker start. See also Docker run reference.

volumes
ports & networking

Interacting with containers

Getting information

* docker-compose commands are shaded in gray. They assume a docker-compose.yml file in the current directory.

See also

How to map Caps Lock to Escape when tapped and Control when held on Mac OS Sierra

Escape and Control are useful keys when using Vim so it's nice to map them to a more convenient key like Caps Lock. I had been using Karabiner to do this, but Karabiner doesn't work on Mac OS Sierra. Fortunately Karabiner-Elements provides a subset of the features planned for the next generation Karabiner including remapping Caps Lock to Escape when tapped and Control when held down. The solution below is from @zeekay on issue #8. I am using Karabiner-Elements 0.91.12 and macOS Sierra 10.12.4.

  • Uninstall Seil and Karabiner, if previously installed
  • Install Karabiner-Elements
    $ brew cask install karabiner-elements 
    
  • Edit ~/.config/karabiner/karabiner.json to be:
    {
        "global": {
            "check_for_updates_on_startup": true,
            "show_in_menu_bar": true,
            "show_profile_name_in_menu_bar": false
        },
        "profiles": [
            {
                "complex_modifications": {
                    "parameters": {
                        "basic.to_if_alone_timeout_milliseconds": 250
                    },
                    "rules": [
                        {
                            "manipulators": [
                                {
                                    "description": "Change caps_lock to control when used as modifier, escape when used alone",
                                    "from": {
                                        "key_code": "caps_lock",
                                        "modifiers": {
                                            "optional": [
                                                "any"
                                            ]
                                        }
                                    },
                                    "to": [
                                        {
                                            "key_code": "left_control"
                                        }
                                    ],
                                    "to_if_alone": [
                                        {
                                            "key_code": "escape",
                                            "modifiers": {
                                                "optional": [
                                                    "any"
                                                ]
                                            }
                                        }
                                    ],
                                    "type": "basic"
                                }
                            ]
                        }
                    ]
                },
                "devices": [],
                "fn_function_keys": {
                    "f1": "display_brightness_decrement",
                    "f10": "mute",
                    "f11": "volume_decrement",
                    "f12": "volume_increment",
                    "f2": "display_brightness_increment",
                    "f3": "mission_control",
                    "f4": "launchpad",
                    "f5": "illumination_decrement",
                    "f6": "illumination_increment",
                    "f7": "rewind",
                    "f8": "play_or_pause",
                    "f9": "fastforward"
                },
                "name": "Default profile",
                "selected": true,
                "virtual_hid_keyboard": {
                    "caps_lock_delay_milliseconds": 0,
                    "keyboard_type": "ansi"
                }
            }
        ]
    }

Alternative

@kbussell created a script to do the same using Hammerspoon1 instead of Karabiner.


  1. Hammerspoon is awesome.

What are Redux selectors? Why use them?

Selectors are functions that take Redux state as an argument and return some data to pass to the component.

They can be as simple as:

const getDataType = state => state.editor.dataType;

Or they can do more complicated data transformations like filter a list.

They are typically used in mapStateToProps1 in react-redux's connect:

For example this

export default connect(
  (state) => ({
    dataType: state.editor.dataType,
  })
)(MyComponent);

becomes this:

export default connect(
  (state) => ({
    dataType: getDataType(state),
  })
)(MyComponent);

Why?

One reason is to avoid duplicated data in Redux.

Redux state can be thought of like a database and selectors like SELECT queries to get useful data from the database. A good example is a filtered list.

If we have a list of items in Redux but want to show a filtered list of the items in the UI, we could filter the list, store it in Redux then pass it to the component. The problem is there are two copies of some of the items and it is easier for data to get out of sync. If we wanted to update an item, we'd have to update it in two places.

With selectors, a filterBy value would be stored in Redux and a selector would be used to compute the filtered list from the Redux state.

Why not perform the data transformations in the component?

Performing data transformations in the component makes them more coupled to the Redux state and less generic/reusable. Also, as Dan Abramov points out, it makes sense to keep selectors near reducers because they operate on the same state. If the state schema changes, it is easier to update the selectors than to update the components.

Performance

mapStateToProps gets called a lot so performing expensive calculations there is not good. This is where the reselect library comes in. Selectors created with reselects's createSelector will memoize to avoid unnecessary recalculations. Note if performance is not an issue, createSelector is not needed. TODO: add note about shallowCompare.

References / See also

I got interested in selectors after watching Dan Abramov's video: https://egghead.io/lessons/javascript-redux-colocating-selectors-with-reducers

Redux docs on selectors: http://redux.js.org/docs/recipes/ComputingDerivedData.html

Reselect: https://github.com/reactjs/reselect


1. mapStateToProps can be considered a selector itself

git worktree notes

git worktree allows you to have multiple working directories associated with one git repo. It has been useful for me to look at, copy code between, and run two different branches of my code. I learned about git worktreee from James Ide's tweet:

Initial setup

Move the checked out git repository to a /main subdirectory

$ mv /my-project /main 
$ mkdir /my-project 
$ mv /main /my-project 

Create a new working tree from an existing branch

$ cd /my-project/main 
$ git worktree add ../my-branch-working-dir my-branch 
$ cd ../my-branch-working-dir 

Create a new branch and new working tree

$ cd /my-project/main 
$ git worktree add -b my-new-branch ../my-new-branch-working-dir origin/master 
$ cd ../my-new-branch-working-dir 

Delete a working tree

$ cd /my-project 
$ rm -rf my-branch-working-dir 
$ git worktree prune 

fatal: my-branch is already checked out error

Deleting .git/worktrees/my-branch fixed the problem for me. See https://stackoverflow.com/q/33296185/101911

Reference / See also

https://github.com/blog/2042-git-2-5-including-multiple-worktrees-and-triangular-workflows

Using Firebase, Next.js, React, Redux, styled-components, and Reactstrap to build a Todo app

Here are some notes on making yet another task manager app using Firebase, Next.js, React, Redux, styled-components, Reactstrap (Bootstrap 4 alpha), Flow, and Prettier. I realize Hacker News clones are the new Todo app, but I built an old Todo app since I'm old. I starting out making a different app, but then I started putting my todo notes in my app so...

The app is here: https://kage.saltycrane.com and the code is here: https://github.com/saltycrane/kage.

Notes

  • Uses Firebase password authentication, Google authentication, and anonymous authentication when a user does not sign in.
  • Uses the Firebase database to persist tasks.
  • Next.js supports server-side rendering but I'm not taking advantage of this because I could not figure out how to configure Firebase auth on the server side.
  • I'm a fan of using Redux for applications. I think it makes complicated interactions easy to follow and debug. However, Redux is not good at handling complicated asynchronous side effects. I use redux-thunk a little to sequence asynchronous actions and async+await in some places. I tried to keep it's usage to a minimum because it is easy to misuse/overuse. I've played with redux-saga and it seems good. Maybe I will use it if I build something more complicated.
  • Uses my own redux-promise-memo library. It contains promise middleware copied from Gluestick, a reducer to store arguments for "memoization" and API status, and a memoize decorator which will prevent firing a promise-based action if has already been fired with the same arguments. "Memoization" is in quotes because it does not manage storing the cached data. It only manages whether to dispatch the action or not. Assume the user has stored the data in Redux.
  • Initially I didn't understand how to use styled-components. Later I realized styled-components could be used in place of sub objects in a styles object for a component when using e.g. Radium or Aphrodite. I like how it makes the JSX look clean. I've heard performance is a weakness of styled-components but they are working to improve it.
  • I'm a former backend developer so I don't know how to make things pretty. I guess I'll go with Bootstrap.
  • Flow has a lot of pain points but in-editor feedback is useful and it helps me write simpler code.
  • Prettier is great.

Other ideas to try


Here's how to try out Next.js on Mac OS X

Install Node.js and Yarn

  • Install Node.js 6.10 LTS using Homebrew
    $ brew install node@6 
    
    This is an alternate version of Node.js so the PATH environment variable needs to be updated to find it. Update ~/.bash_profile to add Node 6 to the beginning of the PATH variable
    $ echo 'export PATH="/usr/local/opt/node@6/bin:$PATH"' >> ~/.bash_profile 
    
    Restart the terminal to source ~/.bash_profile to update the PATH variable. After restarting the terminal, check that Node is installed:
    $ node --version
    v6.10.3
    
  • Install the Yarn package manager using the npm package manager that comes with Node.js
    $ npm install -g yarn 
    
    Verify yarn is installed:
    $ yarn --version
    0.24.5
    

Hello World with Next.js

  • Create a new project folder
    $ mkdir ~/myproject 
    $ cd ~/myproject 
    
  • Create a new file named package.json with the following:
    {
      "dependencies": {
        "next": "3.0.0-beta16",
        "react": "15.6.1",
        "react-dom": "15.6.1"
      },
      "scripts": {
        "dev": "next",
        "build": "next build",
        "start": "next start"
      }
    }
    
  • Install Next.js and React in the local project directory
    $ yarn 
    
  • Create a pages directory and a new file pages/index.js:
    import React from "react";
    
    export default class Hello extends React.Component {
      render() {
        return <div>Hello World</div>;
      }
    }
    
  • Start the dev server
    $ yarn dev 
    
  • Go to http://localhost:3000 in the browser

Flow type cheat sheet

Flow is a static type checker for Javascript. This is a list of Flow types generated from the source code in https://github.com/facebook/flow/tree/v0.52.0/ The script to generate this list is on github. Fixes welcome.

Note: I created a separate section for "private" or "magic" types with a $ in the name. See the note here and comment here. Update: Some these types are now documented here.

Flow version: v0.52.0

Core

Document Object Model (DOM)

Document Object Model (DOM) "private" types

Browser Object Model (BOM)

Browser Object Model (BOM) "private" types

CSS Object Model (CSSOM)

indexedDB

Node.js

Node.js "private" types

Comparing vanilla React, Redux, and Redux Thunk - Chained modals exercise part 3

This is a comparison of three methods for building a sequence of modals using: 1) React only 2) React+Redux and 3) React+Redux+Redux Thunk.

In part 1 and part 2, I created a configurable sequence of modals using React and React Router. Each modal had a form input. Clicking the "Next" button made an AJAX request and, on success, advanced to the next modal. The user was able to navigate between modals using the browser's history. This example adds better management of the form input state. Form input state can be pre-populated from the server and the input state is maintained when navigating forward or backward between modals.

React only

The first solution uses a parent component to manage the state of the chained modals. The full code is here and the demo is here.

App.js

RoutedApp is the top level component which contains the routing configuration. It is unchanged from part 2 except a formData prop is now passed in addition to the modalList prop to allow pre-filling form data from the server. Assume that "Servur" is data provided by the backend server.

This uses ES'15 arrow functions, argument destructuring and JSX spread. [App.js on github]

const RoutedApp = () => (
  <Router history={hashHistory}>
    <Route component={App}>
      <Route path="/" component={
        partial(ChainedModals, {
          modalList: ['/name', '/phone', '/done'],
          formData: {name: 'Servur'}
        })}>
        <Route path="/name" component={ModalName} />
        <Route path="/phone" component={ModalPhone} />
        <IndexRedirect to="/name" />
      </Route>
      <Route path="/done" />
    </Route>
  </Router>
);

const App = ({ children }) => (
  <div>
    <PageBehindModals />
    {children}
  </div>
);

const partial = (Comp, props) => (fprops) => <Comp {...props} {...fprops} />;
ChainedModals.js

ChainedModals manages two things: 1) form input state for all the modals and 2) advancing to the next modal in the sequence. To manage sequencing, the index of the current modal is stored in the component state. Using React's lifecycle methods componentWillMount and componentWillReceiveProps it determines the current index from the route before rendering. When a modal's "Next" button is clicked, it uses the current index to determine the next route and navigates to it.

ChainedModals also stores form data in its component state and defines methods for each modal to store their form data (_storeName and _storePhone). One problem with this approach is that specific modal functionality is defined in ChainedModals so it no longer works with an arbitrary list of modals passed in modalList as it did in parts 1 and 2.

This uses ES'15 nested destructuring and classes, and ES'17 class properties. [ChainedModals.js on github]

class ChainedModals extends Component {
  constructor(props) {
    super(props);
    const { formData } = props;
    this.state = {
      currIndex: null,
      formData: {
        name: null,
        phone: null,
        ...formData
      }
    };
  }

  render() {
    const { children } = this.props;
    const { currIndex, formData } = this.state;

    const modalElement = children && React.cloneElement(children, {
      step: currIndex + 1,
      formData: formData,
      storeName: this._storeName,
      storePhone: this._storePhone,
      gotoNext: this._gotoNext,
      backdrop: false,
      show: true
    });

    return (
      <div>
        <ModalBackdrop />
        {modalElement}
      </div>
    );
  }

  componentWillMount() {
    this._setIndexFromRoute(this.props);
  }

  componentWillReceiveProps(nextProps) {
    this._setIndexFromRoute(nextProps);
  }

  _setIndexFromRoute(props) {
    const { modalList, location: { pathname } } = props;
    const index = modalList.findIndex(path => path === pathname);
    this.setState({currIndex: index});
  }

  _gotoNext = () => {
    const { modalList } = this.props;
    const { currIndex } = this.state;
    const nextRoute = modalList[currIndex + 1];
    hashHistory.push(nextRoute);
  };

  _storeName = (name) => {
    this.setState({
      formData: {
        ...this.state.formData,
        name: name
      }
    });
  };

  _storePhone = (phone) => {
    this.setState({
      formData: {
        ...this.state.formData,
        phone: phone
      }
    });
  };
}
ModalName.js

ModalName changed to accept some form data from its parent and it also still manages the AJAX request and the state related to it.

This uses ES'15 destructuring, classes, and promises, and ES'17 rest/spread and class properties. [ModalName.js on github]

class ModalName extends Component {
  constructor(props) {
    super(props);
    const { formData: { name } } = props;
    this.state = {
      name: name || '',
      isRequesting: false,
      errorMsg: null
    };
  }

  render() {
    const { step, ...rest } = this.props;
    const { name, isRequesting, errorMsg } = this.state;

    return (
      <Modal {...rest}>
        <Modal.Header closeButton>
          <Modal.Title>Step {step} - Name</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          {isRequesting && <p><em>Making fake ajax request...</em></p>}
          {errorMsg && <p><em>{errorMsg}</em></p>}
          <Input
            label="Enter your name"
            type="text"
            bsSize="large"
            {...(errorMsg ? {bsStyle: 'error'} : {})}
            value={name}
            onChange={this._handleInputChange}
            ref={(c) => this._input = c}
          />
        </Modal.Body>
        <Modal.Footer>
          <Button bsStyle="primary" onClick={this._handleClickNext}>Next</Button>
        </Modal.Footer>
      </Modal>
    );
  }

  _handleInputChange = () => {
    this.setState({name: this._input.getValue()});
  };

  _handleClickNext = () => {
    const { storeName, gotoNext } = this.props;
    const name = this._input.getValue();

    this.setState({isRequesting: true, errorMsg: null});
    request('/api/name', name)
      .then(() => {
        storeName(name);
        gotoNext();
      })
      .catch((error) => {
        this.setState({isRequesting: false, errorMsg: error});
      });
  };
}

Redux

Redux provides a way to manage application state outside of components. Examples of application state are the index of the current modal, the form input value, and status of the AJAX request. Using Redux to manage state makes React components more simple and reusable. Redux also makes it easier to manage complex interactions that affect multiple parts of the application.

Like React, data flows in one direction in Redux. Actions describe changes to be made, then reducers make changes to the state based on the actions, finally, the new state is passed to React components via props. Actions are simple objects. Reducers are pure functions that accept a state and action and return a new state. Because reducers are pure functions, they can be composed of many smaller reducers that each operate on a smaller slice of the state. See the Three Principles of Redux.

The code for the redux solution is here and the demo is here.

App.js

A Redux store is created to hold the application state. The state is initialized with modalList and formData that were previously passed into the ChainedModals component. The application element tree is wrapped with Provider which makes the Redux store available to it's child components.

I also added an event handler which dispatches a Redux action whenever the route changes. This idea was taken from this Redux issue and this related Redux pull request. Note: not all imports are shown in the snippet. See all the imports in [App.js on github]

import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { routeChanged } from '../actions';
import reducer from '../reducers';

const initialState = {
  modalList: [
    '/name',
    '/phone',
    '/done'
  ],
  currIndex: null,
  formData: {
    name: 'Servur',
    phone: null
  }
};

const store = createStore(reducer, initialState);

// Dispatch an action when the route changes.
hashHistory.listen(location => store.dispatch(routeChanged(location)));

const RoutedApp = () => (
  <Provider store={store}>
    <Router history={hashHistory}>
      <Route component={App}>
        <Route path="/" component={ChainedModals}>
          <Route path="/name" component={ModalName} />
          <Route path="/phone" component={ModalPhone} />
          <IndexRedirect to="/name" />
        </Route>
        <Route path="/done" />
      </Route>
    </Router>
  </Provider>
);

const App = ({ children }) => (
  <div>
    <PageBehindModals />
    {children}
  </div>
);
actions.js

I defined an action creator for when a route is changed and two for storing data from a user. The action creators return the action which is a simple object that has a type and some other simple data. The reducers will change the state based on the actions. [actions.js on github]

export const ROUTE_CHANGED = 'ROUTE_CHANGED';
export const STORE_NAME = 'STORE_NAME';
export const STORE_PHONE = 'STORE_PHONE';

export function routeChanged(location) {
  return {
    type: ROUTE_CHANGED,
    location: location
  }
}

export function storeName(name) {
  return {
    type: STORE_NAME,
    name: name
  };
}

export function storePhone(phone) {
  return {
    type: STORE_PHONE,
    phone: phone
  };
}
reducers.js

The _sequencing reducer sets the current index based on the route when the route changes. Previously this was done in the ChainedModals component. The _formData reducer stores data (either name or phone) from the user in the state. I wrap statements for each case in curly braces so that const and let declarations will have a more reasonable scope. Thanks Or! [reducers.js on github]

function modalsReducer(state, action) {
  return {
    ..._sequencing(state, action),
    formData: _formData(state.formData, action)
  }
}

function _sequencing(state, action) {
  switch (action.type) {
    case ROUTE_CHANGED: {
      const { location: { pathname } } = action;
      const index = state.modalList.findIndex(path => path === pathname);
      return {
        ...state,
        currIndex: index
      };
    }
    default:
      return state;
  }
}

function _formData(state, action) {
  switch (action.type) {
    case STORE_NAME: {
      return {
        ...state,
        name: action.name
      }
    }
    case STORE_PHONE: {
      return {
        ...state,
        phone: action.phone
      }
    }
    default:
      return state;
  }
}
ChainedModals.js

The ChainedModals component is now connected to Redux. Properties from the Redux state (currIndex, modalList, and formData) are passed into the React component as props with the same name. Similarly, the Redux actions storeName and storePhone are passed in as props with the same name. A lot of the code to manage the form state and sequencing is now removed. However it still defines a _gotoNext method which is used to navigate to the next route. Note in render(), I pull out the children and currIndex props and pass the rest of the props (e.g. formData, storeName, gotoNext) onto the child modal using the ES'17 object rest/spread operators. [ChainedModals.js on github]

class ChainedModals extends Component {
  render() {
    const { children, currIndex, ...rest } = this.props;

    const modalElement = children && React.cloneElement(children, {
      step: currIndex + 1,
      backdrop: false,
      show: true,
      gotoNext: this._gotoNext,
      ...rest
    });

    return (
      <div>
        <ModalBackdrop />
        {modalElement}
      </div>
    );
  }

  _gotoNext = () => {
    const { currIndex, modalList } = this.props;
    const nextRoute = modalList[currIndex + 1];
    hashHistory.push(nextRoute);
  };
}

export default connect(
  function mapStateToProps(state) {
    const { currIndex, modalList, formData } = state;
    return { currIndex, modalList, formData };
  },
  function mapDispatchToProps(dispatch) {
    return {
      storeName: (...args) => dispatch(storeName(...args)),
      storePhone: (...args) => dispatch(storePhone(...args))
    }
  }
)(ChainedModals);
ModalName.js

The individual modal components remain the same. They still make the ajax calls and manage the state related to that. [ModalName.js on github]

class ModalName extends Component {
  constructor(props) {
    super(props);
    const { formData: { name } } = props;
    this.state = {
      name: name || '',
      isRequesting: false,
      errorMsg: null
    };
  }

  render() {
    const { step, ...rest } = this.props;
    const { name, isRequesting, errorMsg } = this.state;

    return (
      <Modal {...rest}>
        <Modal.Header closeButton>
          <Modal.Title>Step {step} - Name</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          {isRequesting && <p><em>Making fake ajax request...</em></p>}
          <Input
            label="Enter your name"
            type="text"
            bsSize="large"
            {...(errorMsg ? {bsStyle: 'error'} : {})}
            help={errorMsg && <em>{errorMsg}</em>}
            value={name}
            onChange={this._handleInputChange}
            ref={(c) => this._input = c}
          />
        </Modal.Body>
        <Modal.Footer>
          <Button bsStyle="primary" onClick={this._handleClickNext}>Next</Button>
        </Modal.Footer>
      </Modal>
    );
  }

  _handleInputChange = () => {
    this.setState({name: this._input.getValue()});
  };

  _handleClickNext = () => {
    const { storeName, gotoNext } = this.props;
    const name = this._input.getValue();

    this.setState({isRequesting: true, errorMsg: null});
    request('/api/name', name)
      .then(() => {
        storeName(name);
        gotoNext();
      })
      .catch((error) => {
        this.setState({isRequesting: false, errorMsg: error});
      });
  };
}

Redux Thunk

Using Redux added some boilerplate but made React components simpler by moving their state to the Redux store. However there is still some state and logic managed by the components. Redux Thunk is middleware for Redux that enables creating actions with side effects such as making asynchronous requests and changing the route. Redux Thunk legitimizes the pattern of providing dispatch to actions. See the Async Actions Redux documentation for detailed information about using Redux Thunk for asynchronous actions. Other alternatives for handling asynchronous actions are writing custom Redux middleware or using Redux Saga.

The code for the redux-thunk solution is here and the demo is here.

App.js

App.js is the same except I applied the redux thunk middleware. [App.js on github]

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

const store = createStore(reducer, initialState, applyMiddleware(thunk));
actions.js

In actions.js I created three action creators with side effects: storeName and storePhone make asynchronous requests gotoNext changes the route. Note: though I used getState in my action creators, in general this is considered an anti-pattern. [actions.js on github]

export const ROUTE_CHANGED = 'ROUTE_CHANGED';
export const STORE_NAME_REQUESTED = 'STORE_NAME_REQUESTED';
export const STORE_NAME_SUCCEEDED = 'STORE_NAME_SUCCEEDED';
export const STORE_NAME_FAILED = 'STORE_NAME_FAILED';
export const STORE_PHONE_REQUESTED = 'STORE_PHONE_REQUESTED';
export const STORE_PHONE_SUCCEEDED = 'STORE_PHONE_SUCCEEDED';
export const STORE_PHONE_FAILED = 'STORE_PHONE_FAILED';

export function routeChanged(location) {
  return {
    type: ROUTE_CHANGED,
    location: location
  }
}

export function gotoNext() {
  return (dispatch, getState) => {
    const { currIndex, modalList } = getState();
    const nextRoute = modalList[currIndex + 1];
    hashHistory.push(nextRoute);
  }
}

export function storeName(name, onSuccess) {
  return dispatch => {
    dispatch(_storeNameRequested());
    return request('/api/name', name)
      .then(() => {
        dispatch(_storeNameSucceeded(name));
        onSuccess();
      })
      .catch(error => {
        dispatch(_storeNameFailed(error));
      });
  }
}

export function storePhone(phone, onSuccess) {
  return dispatch => {
    dispatch(_storePhoneRequested());
    return request('/api/phone', phone)
      .then(() => {
        dispatch(_storePhoneSucceeded(phone));
        onSuccess();
      })
      .catch(error => {
        dispatch(_storePhoneFailed(error));
      });
  }
}

function _storeNameRequested() {
  return {
    type: STORE_NAME_REQUESTED
  };
}

function _storeNameSucceeded(name) {
  return {
    type: STORE_NAME_SUCCEEDED,
    name: name
  };
}

function _storeNameFailed(errorMsg) {
  return {
    type: STORE_NAME_FAILED,
    errorMsg: errorMsg
  };
}

function _storePhoneRequested() {
  return {
    type: STORE_PHONE_REQUESTED
  };
}

function _storePhoneSucceeded(phone) {
  return {
    type: STORE_PHONE_SUCCEEDED,
    phone: phone
  };
}

function _storePhoneFailed(errorMsg) {
  return {
    type: STORE_PHONE_FAILED,
    errorMsg: errorMsg
  };
}
reducers.js

reducers.js now updates some state based on the status of the API request which is used by the modal components to show the spinner or validation errors. [reducers.js on github]

function modalsReducer(state, action) {
  return {
    ..._sequencing(state, action),
    formData: _formData(state.formData, action)
  }
}

function _sequencing(state, action) {
  switch (action.type) {
    case ROUTE_CHANGED: {
      const { location: { pathname } } = action;
      const index = state.modalList.findIndex(path => path === pathname);
      return {
        ...state,
        requestStatus: null,
        currIndex: index
      };
    }
    case STORE_NAME_REQUESTED:
    case STORE_PHONE_REQUESTED: {
      return {
        ...state,
        isRequesting: true,
        errorMsg: null
      }
    }
    case STORE_NAME_SUCCEEDED:
    case STORE_PHONE_SUCCEEDED: {
      return {
        ...state,
        isRequesting: false,
        errorMsg: null
      }
    }
    case STORE_NAME_FAILED:
    case STORE_PHONE_FAILED: {
      return {
        ...state,
        isRequesting: false,
        errorMsg: action.errorMsg
      }
    }
    default:
      return state;
  }
}

function _formData(state, action) {
  switch (action.type) {
    case STORE_NAME_SUCCEEDED: {
      return {
        ...state,
        name: action.name
      }
    }
    case STORE_PHONE_SUCCEEDED: {
      return {
        ...state,
        phone: action.phone
      }
    }
    default:
      return state;
  }
}
ChainedModals.js

ChainedModals is now a simpler functional component. It's purpose is connecting child modal components to redux and setting some default props for the modals. It is also used to display the backdrop behind the modals. [ChainedModals.js on github]

const ChainedModals = ({ children, ...rest }) => {
  const modalElement = children && React.cloneElement(children, rest);
  return (
    <div>
      <ModalBackdrop />
      {modalElement}
    </div>
  );
};

export default connect(
  function mapStateToProps(state) {
    const { currIndex, isRequesting, errorMsg, formData } = state;
    return {
      backdrop: false,
      show: true,
      step: currIndex + 1,
      isRequesting,
      errorMsg,
      formData
    };
  },
  function mapDispatchToProps(dispatch) {
    return {
      gotoNext: (...args) => dispatch(gotoNext(...args)),
      storeName: (...args) => dispatch(storeName(...args)),
      storePhone: (...args) => dispatch(storePhone(...args))
    }
  }
)(ChainedModals);
ModalName.js

Individual modal components now only use state for controlled inputs. [ModalName.js on github]

class ModalName extends Component {
  constructor(props) {
    super(props);
    const { formData: { name } } = props;
    this.state = {
      name: name || ''
    };
  }

  render() {
    const { step, isRequesting, errorMsg, ...rest } = this.props;
    const { name } = this.state;

    return (
      <Modal {...rest}>
        <Modal.Header closeButton>
          <Modal.Title>Step {step} - Name</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          {isRequesting && <p><em>Making fake ajax request...</em></p>}
          <Input
            label="Enter your name"
            type="text"
            bsSize="large"
            {...(errorMsg ? {bsStyle: 'error'} : {})}
            help={errorMsg && <em>{errorMsg}</em>}
            value={name}
            onChange={this._handleInputChange}
            ref={(c) => this._input = c}
          />
        </Modal.Body>
        <Modal.Footer>
          <Button bsStyle="primary" onClick={this._handleClickNext}>Next</Button>
        </Modal.Footer>
      </Modal>
    );
  }

  _handleInputChange = () => {
    this.setState({name: this._input.getValue()});
  };

  _handleClickNext = () => {
    const { storeName, gotoNext } = this.props;
    const name = this._input.getValue();
    storeName(name, gotoNext);
  };
}

References / Further Reading (mostly from Redux author, Dan Abramov)