Server-Render like a Pro /w Redux-First Router in 10 steps

1. SETUP EXPRESS TO SERVE YOUR BUILD

First we create our universal bundles in code via webpack (NOT via webpack at the command line).

We use 3 express middlewares to give us hot module replacement (HMR) on both the client and server:

server/index.js
webpackHotServerMiddleware passed to app.use will call your server bundle for all requests, i.e. as if the path you set was *.

2. SETUP 2 WEBPACK CONFIGS (SERVER + CLIENT)

The primary thing to note is the inputs and outputs.

The input is the entry which is src/index.js on the client and server/render.js on the server. For the server, it’s not index because webpackHotServerMiddleware in step 1 is already in server/index.js

The output is the value of output.path, which is buildClient/ and buildServer/ respectively.

client config / server config
Things like entries for react-hot-loader are missing for brevity, but you can find them in the complete Redux-First Router demo, along with the production version of step 1, etc.

3. renderToString

This is the entry point of the server bundle.

It’s a function returning a function. That’s what webpackHotServerMiddleware expects for server HMR to work. Put that in the back of your mind, don’t stress it, and keep moving.

server/render.js

What’s going on here is the usual:

  • configure your store (we’ll get to that soon)
  • render your app to string
  • prepare you state for rehydration on the client by converting it to a JSON string
  • send the basics to the client

Part 2: server/configureStore.js

4. thunk: data-fetching

server/configureStore.js
  1. Create your in-memory history using the request path visited as the one and only entry. The history object will live and die with a single request.
  2. pass the history to the same configureStore you use on the client
  3. receive your store and thunk (we'll see why that's returned soon)
  4. await on the the thunk
  5. set the status based on the location.type state

Awaiting on the thunk is the key ingredient. Redux-First Router makes resolving any data-dependencies for the current route seamless.

It’s semi-automatically handled because:

  • Redux middleware is not async
  • it allows you to do custom work before + after, as you’ll see below

If you’re new to Redux-First Router, here’s what the configureStore shared from the client looks like:

src/configureStore.js
Notice an object containing store + thunk are returned similar to connectRoutes. connectRoutes is RFR’s primary kick-off mechanism.

And here’s what the routesMap might looks like:

src/routesMap.js

You will end up with lots of routes, many with their own thunk. If you’re new to Redux-First Router, the idea is the thunk will be called any time a route is visited. A route is visited when its corresponding action type — in this case LIST — is dispatched.

On the server, Redux-First Router does the work of determining which route matches the visited path, and gives you its thunk to await on.

NOTE: on the client, the calling of the thunk is fully automated! To learn more about data-fetching with Redux-First Router, read the data-fetching article.

Part 3

At this point you’re technically done, but you’re not a pro yet. Let’s handle some common things you might wanna do.

5. REDIRECTS (onBeforeChange)

server/configureStore.js (modified)

Here we detect if the state.location.kind is a redirect, and if so, return false.

In server/render.js bundled by webpack, simply short-circuit if you don’t receive a store:

server/render.js

We can do that because doesRedirect already handled the redirect on the express side with res.redirect(302, pathNameReceivedFromRFR).

How do you create redirects in the first place?

options.js — redirect is simple action creator built into RFR

In the options provided to connectRoutes(), you can specify an onBeforeChange handler that is called before every route change. Its signature is basically that of a thunk.

NOTE: you must use the third argument which is a pending action since it won’t exist in state yet, and it might never if you redirect. isAllowed is basically a redux Selector, except we preemptively pass the current location’s type.

If you dispatch a redirect here, the Redux-First Router middleware will short-circuit the action that triggered this and immediately start handling the new route, in this case, LOGIN.

We determine that we should redirect via the isAllowed function which takes the current type (aka route) as well as all the current state. isAllowed could do anything.

What you should focus on is how it takes the entire state as an argument. Within that state you can have information about your user to determine if they are in the correct role to view the current type/route.

The idea here in universal rendering is that we use Redux state in an identical way to the client for maximum code re-use. We’ll cover below how to pre-populate your store with user state soon.

6. REDIRECTS (route-level)

You can also specify redirects at the thunk level like this:

src/routesMap.js

Notice we detect that the same data received previously is empty, so we redirect to the same route with a different payload (category: ‘all’) which is guaranteed to always have data and therefore not redirect.

There’s all sorts of reasons to use redirects at the route level. Just know you can use redirect in your thunks to achieve it.

Lastly, for completeness, you also want to short-circuit your thunks if Redux already has the data you need:

src/routesMap.js

This will only occur on the client as a result of the user navigating back to a route already visited. Perhaps you have a fast-paced app and the data may have changed, in which case you don’t want to do that — what you do here is up to you.

7. REDIRECTS (OPTIMIZE)

server/configureStore.js

If you’re using both route level and global onBeforeChange redirects, you likely want to check for redirects before and after your thunk. This way you don’t unnecessarily wait on data (bef0re) and address any route-level redirects (after).

  1. The first redirect will be caused by onBeforeChange, for example detecting the user is not allowed.
  2. The second redirect will be detected as a result of the thunk resolving.

Your logic may be such that the corresponding thunk in fact makes no asynchronous requests, and quickly determines it should redirect. So don’t assume reaching this stage means more than one tick needs to have gone by. This can all happen very fast.

The primary difference between onBeforeChange is that you moved the redirect detection logic to just the route that has these unique needs.

8. FETCH APP-WIDE DATA

Sometimes you want to fetch app-wide data just as you would without Redux-First Router. Here’s where you do it:

server/configureStore.js

You can do it before or after await thunk(store), depending on whether the app-wide thunks depend on the data received from the route thunk. Or you can put all 3 in the same Promise.all if they neither are dependent on each other’s triggered state. Here are the considerations:

  1. If the route thunk depends on the app-wide thunks, put the route thunk after
  2. if the app-wide thunk depends on the route thunk, put the app-wide thunk after
  3. if neither are dependent on each other, wrap em in a single Promise.all

9. JSON Web Token in preLoaded state

If you’re using JSON Web Tokens, you can pre-populate your store’s state with one to obtain all your user state so that onBeforeChange filters the user’s request.

server/configureStore.js

Then in your onBeforeChange handler you get the user from state, using the jwToken which will convert to a user object:

src/options.js
  • If there is no user, you redirect to the LOGIN route
  • if there is a user, but there wasn’t one until now (which will always be the case on the server), dispatch an action to populate the store with the discovered user. Keep in mind this will happen before any route actions are dispatched, which means they will be able to make use of this user data.

Here’s what your user reducer looks like:

src/reducers/user.js

How do we actually get the user from the JSON Web Token?

src/selectors/userFromState.js

We use a library such as jsonwebtoken which allows us to synchronously transform the JWT into a user object.

It must be synchronous since this is happening in the Redux middleware, which only operates synchronously. Make sure you pick a library that offers sync.
arbitrary key/val on route object

We’re doing a bit more than usual here, but the idea is that this is the isAllowed function/selector from earlier. Just a more advanced one that verifies your JWT. It checks for a role attached to a route in your routesMap. You can put any arbitrary key/val on your routes for later filtering.

The filtering functions, isAllowed or userFromState, would be in your selectors directory because they also double as a more straightforward selector on the the client when you simply want to retrieve the user from state while simultaneously determining if the user has access to the given route.

So what we’re doing here is:

  • get the user on either the server or client
  • get any required roles for the current route
  • if there is no role, return the user whose truthy value will indicate the user has permission to view the route
  • If there’s no user, return null
  • if there is a role for the route, return the user if the user has that role
If you aren’t familiar with JSON Web Tokens, the idea is you can store small amounts of data cryptographically in a “token” (i.e. long hash string) that can be decoded into the essential data about your user on the server without having to make additional async requests to databases to get it. Perf wins! To facilitate that, you’re essentially storing all your user info secretly in the browser. But only you have the ability to transform it to a user object on the server via your secret key. Strategies without SSR will store the JWT in localstorage instead of a cookie and pass the JWT in ajax request headers. Cookies are required for SSR because unlike with ajax requests, you can’t set headers when users visit your site directly. In short, localStorage is for SPAs (which are dead) and cookies for Universal apps (what you want for all that Google traffic).

To bring this full circle, the idea is

  1. createStore(rootReducer, preloadedState, enhancers) will start out with just the JWT via preloadedState
  2. and end up /w a user object before your route thunks have a chance to resolve, allowing them to determine what to fetch + what state to trigger
  3. OR the user will be redirected to the LOGIN route ⛩

10. TRADITIONAL USER COOKIE STRATEGY

You can also do the traditional user cookie strategy where you use the cookie to fetch the complete user object from the database.

Performance wise it’s not as good, because it means requests to the database are needed on every authenticated route.
server/configureStore.js

What you’re seeing here is 2 things:

  1. we are using a non-RFR thunk fetchUser to get the user’s info and store it in Redux state based on the sessionId cookie
  2. we are delaying the initial dispatch until after we potentially have the user in state, so that proper filtering (global or route level) can occur.

The rest you’ve seen before.

Finally, to make this actually work we must consider that RFR by default automatically dispatches the initial action when you call connectRoutes. There’s an option to delay it so you can do it manually. When you do so, you are returned an initialDispatch function as well:

src/configureStore.js

As you can see, we have you covered for a great number of circumstances. There’s very little magic, however there are helpers to make the idiom here obvious. In other words, it’s flexible while requiring the least amount of code.

NOTE: This strategy was perfected while working on @eudaimos’ use case. initialDispatch actually first originated for the Saga's use case of being able to trigger the first route after sagas have started, something @KaRky pointed out.

CONCLUSION

By doing things the Redux First Router way, you can encapsulate all your needs in Redux (both client and server). AKA, Universal Redux™.

It’s really quite a powerful mechanism — it means you virtually have no server code outside of the Redux/React code you’re already using on the client.

The number of ways to do things “right” is becoming less and less, day by day, which makes code more readable for all, and means less decisions to make.

In the future we’ll cover complete user authentication, not just filtering. Subscribe to the Reactlandia publication on Medium to stay tuned.

If this is your first time encountering Redux-First Router, make sure to review and star the repo here: