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:
app.usewill 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
webpackHotServerMiddleware in step 1 is already in
The output is the value of
output.path, which is
Things like entries for
react-hot-loaderare missing for brevity, but you can find them in the complete Redux-First Router demo, along with the production version of step 1, etc.
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
configureStoreyou use on the client
- receive your
thunk(we'll see why that's returned soon)
awaiton the the
- set the status based on the
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
thunkare returned similar to
connectRoutesis 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.
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.
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
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
NOTE: you must use the third argument which is a pending
actionsince it won’t exist in state yet, and it might never if you redirect.
isAllowedis 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,
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
redirectin 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
thunkdepends on the app-wide
thunks, put the route
- if the app-wide
thunkdepends on the route
thunk, put the app-wide
- if neither are dependent on each other, wrap em in a single
JSON Web Token in
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 the
- 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,
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
userwhose 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
userobject on the server via your
secretkey. Strategies without SSR will store the JWT in
localstorageinstead 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,
localStorageis for SPAs (which are dead) and
cookiesfor 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 via
- and end up /w a
userobject 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
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
fetchUserto get the user’s info and store it in Redux state based on the
- 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.
initialDispatchactually first originated for the Saga's use case of being able to trigger the first route after sagas have started, something @KaRky pointed out.
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 🚀
Redux-First Router data-fetching: solving the 80% use case for async Middleware
Understanding the architectural decisions behind the tools you are using is perhaps more important than the many things…
Pre Release: Redux-First Router — A Step Beyond Redux-Little-Router
The purpose of this article is to debunk the effectiveness of route-matching components + nested routes when using…
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 👇🏽