Redux Promise Middleware - peter-mouland/react-lego-2017 GitHub Wiki
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.
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);
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.
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