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:
webpackHotServerMiddleware
passed toapp.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.
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.
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
- 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.
- pass the history to the same
configureStore
you use on the client - receive your
store
andthunk
(we'll see why that's returned soon) await
on the thethunk
- 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:
Notice an object containing
store
+thunk
are returned similar toconnectRoutes
.connectRoutes
is RFR’s primary kick-off mechanism.
And here’s what the routesMap
might looks like:
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)
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:
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?
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:
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:
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)
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).
- The first redirect will be caused by
onBeforeChange
, for example detecting the user is not allowed. - 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:
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:
- If the route
thunk
depends on the app-widethunks
, put the routethunk
after - if the app-wide
thunk
depends on the routethunk
, put the app-widethunk
after - 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.
Then in your onBeforeChange
handler you get the user from state, using the jwToken
which will convert to a user object:
- If there is no
user
, you redirect to theLOGIN
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:
How do we actually get the user from the JSON Web Token?
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.
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 yoursecret
key. Strategies without SSR will store the JWT inlocalstorage
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) andcookies
for Universal apps (what you want for all that Google traffic).
To bring this full circle, the idea is
createStore(rootReducer, preloadedState, enhancers)
will start out with just the JWT viapreloadedState
- 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 - 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.
What you’re seeing here is 2 things:
- we are using a non-RFR thunk
fetchUser
to get the user’s info and store it in Redux state based on thesessionId
cookie - 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:
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:
please like, star and tweet so other developers know whatsup 🚀
> For more idiomatic javascript in Reactlandia, read:
Find me on twitter @faceyspacey
Want to stay current in Reactlandia? Tap/click “FOLLOW” next to the FaceySpacey publication to receive weekly Medium “Letters” via email 👇🏽