Adding state management with Redux in a CRA + SSR project

Or how to initialize your Redux store on the server, then pick it up and hydrate it on the client.

Andrei Duca
Jan 28, 2018 · 7 min read

This is part 2 of my CRA+SSR series:

  1. Upgrading a create-react-app project to SSR + code splitting
  2. Adding state management with Redux in a CRA + SSR project

I personally like Redux because it uses a single object to represent the entire state of the application. This single object is also composable, so you can split its management into smaller, independent bits. I also find it useful to have a one-way data flow, so when I click a button in one place, I trigger a chain of events, ending with the update of the UI based on the new state, maybe even in another place of the app, without having to pass props and execute callbacks all around.

If you’re not too familiar with Redux and how it works, I suggest you read this quick article for a birds-eye view, or go into much more detail and even find how it’s different than other libraries.

What we’ll cover in this article:

  1. Adding Redux on the client
  2. Adding Redux on the server
  3. Rehydrating the client store from the server

First: Client side

yarn add redux react-redux redux-thunk

We need to create a reducer in /src/store/appReducer.js, aka a pure function taking two arguments (the previous state, and an action/modifier object) and returning the new state as an immutable object.

const initialState = {
message: null,
};
export const appReducer = (state = initialState, action) => {
switch(action.type) {
case 'SET_MESSAGE':
return {
...state,
message: action.message,
};
default:
return state;
}
};

We’re destructuring the state object here to keep the old properties unchanged, and only replace what we need. In our case, this is not necessarily needed, as we only have the message property, but in a larger application this is what you typically do.

Let’s also write an action creator, that is a function that returns an action object. I usually like to keep things together, so we’ll add this in our reducer file. But feel free to create a separate file if you want to group things differently.

export const setMessage = messageText => ({ type: 'SET_MESSAGE', message: messageText });

Now we’ll create our store initializer in /src/store/configureStore.js.

import {
createStore,
combineReducers,
compose,
applyMiddleware,
} from 'redux';
import ReduxThunk from 'redux-thunk'import { appReducer } from './appReducer';// if you're using redux-thunk or other middlewares, add them here
const createStoreWithMiddleware = compose(applyMiddleware(
ReduxThunk,
))(createStore);
const rootReducer = combineReducers({
app: appReducer,
});
export default function configureStore(initialState = {}) {
return createStoreWithMiddleware(rootReducer, initialState);
};

We’re wrapping our createStore in a function so we can pass the initial state when initializing. This will help us when hydrating the state from the server.

Now let’s use it in our app. Wrap the main App component in a Redux provider in /src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';
import { Provider as ReduxProvider } from 'react-redux'
import App from './App';
import configureStore from './store/configureStore';
const store = configureStore();const AppBundle = (
<ReduxProvider store={store}>
<App />
</ReduxProvider>
);
window.onload = () => {
Loadable.preloadReady().then(() => {
ReactDOM.hydrate(
AppBundle,
document.getElementById('root')
);
});
};

Next, we’ll display the message in our App. We’ll also set a default message on the client if the initial value is empty.

import { connect } from 'react-redux';
import { setMessage } from './store/appReducer';
class App extends Component {
componentDidMount() {
if(!this.props.message) {
this.props.updateMessage("Hi, I'm from client!");
}
}
render() {
return (
<div className="App">
// ...
<p>
Redux: { this.props.message }
</p>

</div>
);
}
}
export default connect(
({ app }) => ({
message: app.message,
}),
dispatch => ({
updateMessage: (txt) => dispatch(setMessage(txt)),
})
)(App);

That’s it! Now run the app with yarn start and see the “Hi, I’m from client!” message displayed after the app loads.

Next: Server side

export default (store) => (req, res, next) => {
// ...
const html = ReactDOMServer.renderToString(
<ReduxProvider store={store}>
<App />
</ReduxProvider>
);
// ...
}

Now we need to initialize our store and pass it as a prop when using the renderer middleware in our router (in /server/index.js):

import serverRenderer from './middleware/renderer';
import configureStore from '../src/store/configureStore';
//...const store = configureStore();
router.use('^/$', serverRenderer(store));
// ...

In a real application, you will want to move this code in a controller, so you decouple the logic of the app from the initialization of the express server. Also, you’ll want some controller actions that hold more complex logic, maybe even based on the request url. In fact, let’s do this now. We’ll move the code for the router initialization and we’ll write an index action that handles the Redux store initialization in /server/controllers/index.js:

import express from "express";

import serverRenderer from '../middleware/renderer';
import configureStore from '../../src/store/configureStore';

const router = express.Router();
const path = require("path");


const actionIndex = (req, res, next) => {
const store = configureStore();
serverRenderer(store)(req, res, next);
};


// root (/) should always serve our server rendered page
router.use('^/$', actionIndex);

// other static resources should just be served as they are
router.use(express.static(
path.resolve(__dirname, '..', '..', 'build'),
{ maxAge: '30d' },
));

export default router;

As we moved the code in a subdirectory, please make sure that in the route for static files you add an extra ‘..’, so path.resolve() will point to the right location.

Our action is just another middleware that will call the serverRenderer middleware after the Redux store has been initialized. We can even dispatch an action before calling the renderer.

import { setMessage } from '../../src/store/appReducer';// ...const actionIndex = (req, res, next) => {
const store = configureStore();
store.dispatch(setMessage("Hi, I'm from server!"));
serverRenderer(store)(req, res, next);
};
// ...

Now we can clean our /server/index.js entry point:

import express from 'express';
import indexController from './controllers/index';
const app = express();
app.use(indexController);
// start the app
// ...

Build the app and start the node server. You should see the message rendered on the server before the client app initializes.

yarn build && node server/bootstrap.js

Finally: Rehydrate the client store from the server

Because we’re already writing code in our HTML on the server, let’s send the data in the same place. We’ll add a placeholder in /public/index.html:

<div id="root"></div>

<script type="text/javascript" charset="utf-8">
window.REDUX_STATE = "__SERVER_REDUX_STATE__";
</script>

Now, let’s replace this on the server with a JSON representation of our data. Update the serverRenderer:

export default (store) => (req, res, next) => {
// ...
const html = ReactDOMServer.renderToString(
<ReduxProvider store={store}>
<App />
</ReduxProvider>
);
const reduxState = JSON.stringify(store.getState()); // ... return res.send(
htmlData
.replace(
'<div id="root"></div>',
`<div id="root">${html}</div>`
)
.replace(
'</body>',
extraChunks.join('') + '</body>'
)
.replace('"__SERVER_REDUX_STATE__"', reduxState)
);
}

OK, now we’ll pick this up on the client and initialize the store before rendering the app. Update /src/index.js:

const store = configureStore( window.REDUX_STATE || {} );const AppBundle = (
<ReduxProvider store={store}>
<App />
</ReduxProvider>
);

Build the app and start the node server one last time.


Going further

// src/store/appReducer.js
export const setAsyncMessage = messageText => dispatch => (
new Promise((resolve, reject) => {
setTimeout(() => resolve(), 2000);
})
.then(() => dispatch(setMessage(messageText)))
);
// server/controllers/index.js
const actionIndex = (req, res, next) => {
const store = configureStore();

store.dispatch(setAsyncMessage("Hi, I'm from server!"))
.then(() => {
serverRenderer(store)(req, res, next);
});
};

While this works, I personally discourage its use. The main idea behind SSR is to render the app with a minimum initial state, so the user sees something until the app loads in the browser. Also, client-side applications should only hold the logic (aka data manipulation), while the actual data can be fetched async from whatever remote backend / API source.

Many production apps prefer this approach: Facebook, Slack etc. They render an empty application “shell”, then they fetch the data asynchronously and show it when it’s downloaded.

On Snipit.io, I personally use this technique as well. The only data I place in the store on the server is wether the user is logged in or not. Based on that, I load a separate UI on the client. Then, only after the app is initialized on the client, I get the data asynchronously from the backend API. Until the data is fetched, the user will see placeholders for the content that will be renderer. This way, the UI loads almost instantly so the entire perceived performance is better.


What do you think about the techniques explained in these articles? Do you find them useful for your project? Let me know in the comments.

You can also follow me here on Medium or on Twitter @andreiduca for more stories like this.

BucharestJS

BucharestJS is a developer group for anyone interested in Javascript. All skill levels are welcome. You can find us on Meetup.com/BucharestJS, Facebook.com/Groups/BucharestJS and Twitter.com/BucharestJS.

Andrei Duca

Written by

Frontend Engineer | creator of Snipit.io

BucharestJS

BucharestJS is a developer group for anyone interested in Javascript. All skill levels are welcome. You can find us on Meetup.com/BucharestJS, Facebook.com/Groups/BucharestJS and Twitter.com/BucharestJS.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade