Skip to content

Redux Promise Middleware

Peter Mouland edited this page Sep 9, 2016 · 6 revisions

https://github.com/peter-mouland/react-lego/compare/redux...redux-promised

This was an important feature to add, as without it, we would either fetch data on the server that may not be needed or we would have to forget about data-driven pages on the server and leave rendering to the client.

With the promise middleware, components can inform the server about what data is required using existing Redux actions.

This solution was built after reading this excellent article from Smashing Magazine as well as this blog post on Medium by Milo Mordaunt

## Steps :

  • Hook Promise middleware into Redux
  • Check if the component required needs data
    • fetch only the data that is needed,
    • wait for the promise to finish
  • render the hydrated page on the server
    • send the page and the initial data to the client

A 'timeout' limit has also been set, which means if the server takes too long, the app is rendered without the data and instead fetched on the client.

Hook Promise middleware into Redux

The Promise middleware enables us to check to see if the data being sent via the Redux action contains a Promise. If it doesn't the middleware will ignore the action.

// promiseMiddleware.js
export default function promiseMiddleware() {
  return next => action => {
    const { promise, type, timeoutMs = 15000, ...rest } = action;
    if (!promise) return next(action);
    ...
  }
}

If the action does contain a promise, them we will set the state automatically depending on the status of the promise:

  • loading
  • timeout
  • error

The timeout status is interesting. I've decided on a default timeout of 15s, but this can be set on a per action basis : i.e.

// example-action-creator.js
import api from '../api';
export function saveStatsSnapshot(players) {
  return {
    type: SAVE_STATS_SNAPSHOT,
    timeoutMs: 90000,
    promise: api.saveStatsSnapshot(players)
  };
}

Once a timeout happens, the server will respond without the data. This means that our client must handle this, maybe show a message or try again itself. Below shows us using componentDidMount to fetch the data again.

//example-data-driven-component
import { fetchStatsSnapshots } from '../../actions';

class StatsSnapshots extends React.Component {
  static needs = [fetchStatsSnapshots];

  componentDidMount() {
    if (this.props.statsSnapshots.data) return;
    this.props.fetchStatsSnapshots().then(() => {
      this.setState({ error: false });
    });
  }

  render() {
    const { data, status, error } = this.props.statsSnapshots;

    if (!data || status.isLoading) {
      return <h3>Loading Stats-Snapshots...</h3>;
    } else if (status.isError) {
      return <h3>ERROR Loading Stats-Snapshots...</h3>;
    } 
    return (<div> {
            data.map(snapshot => <div key={snapshot.id}>{snapshot.title}</div>)
    } </div>);
}

function mapStateToProps(state) {
  return { statsSnapshots: state.statsSnapshots };
}

export default connect(mapStateToProps, { fetchStatsSnapshots })(StatsSnapshots);

Check if the component required needs data

When setting up the server, we need to get the data before executing next(). This can easily be done within a promise or callback once the data has been fetched. It's getting the correct data that is the interesting part.

React-lego uses a static property called needs which is set to the redux-action which will fetch the data that the component needs. Later, we can use this within our server code to check for this property and execute the action.

// fetchComponentData.js

export default function fetchComponentData(dispatch, components, params) {
  const componentsWithNeeds = [];

  // check if we have a component which needs data
  const needs = components.reduce((prev, current) => {
    const wrapper = current.WrappedComponent;
    if (current.needs) {
      componentsWithNeeds.push(wrapper ? wrapper.name : current.name);
    }
    return current ? (current.needs || []).concat(prev) : prev;
  }, []);

  // wait for the fetch-promise to finish
  return Promise.all(needs.map(need => dispatch(need(params))));
}

The above code is using wrappedComponent property as we expect connect from Redux to be used.

Render the hydrated page on the server

The above code make clever use of the stores dispatch method. This ensures that the store is updated with the new data once the all promises are complete. This allows us to then getState() of our store on the server and set our initialContent.

// set-router-context.js

const store = configureStore();
const setContext = () => {
   const InitialComponent = (
     <Provider store={store}>
       <RouterContext {...renderProps} />
     </Provider>
   );
   res.initialState = store.getState(); // eslint-disable-line
   res.routerContext = res.renderPageToString(InitialComponent); // eslint-disable-line
   next();
};

fetchComponentData(store.dispatch, renderProps.components, renderProps.params)
   .then(setContext)

To see how to bring this altogether for an app see the comparison branch : https://github.com/peter-mouland/react-lego/compare/redux...redux-promised