Isomorphic application with React/Redux
Introduction
So I wanted to build an isomorphic/universal web-application…
This will be a long document about the how and the why
The web-app was greatly influenced by this Viktor Turskyi’s post.
Unlike most articles, I won’t produce here any code example.
I will try to focus on how different piece of code put together will solve building an universal applications problems.
It’s my first take on this kind of application, so I’m sure there are many flaws & rooms for improvement.
But hey! we need a start in order to advance 🏃♀️
prerequisite
You should have some notions with:
- React
- what is a Component
- what is a High-Order Component (HoC)
- Redux
- what is a store
- what is an action
- what is a reducer
- how to use react-redux to connect our React components to the store
- Some javascript tooling:
purpose of server rendering
Server rendering seems a good idea for 2 main reasons:
- make our first render quicker
- support no-JS environments
For this we need to:
- grab the right components to render (using the React methods for server rendering)
a non exiting route means rendering the 404 component - make sure that the components have the right data to begin with.
- pass everything to the client
- after that the client will initialize and run as a single page application
the API
The web-application will interact with an API (packages/api
) which will not be detailed here.
The only thing we need to know about the API is that:
- it’s a REST like API (uses only GET & POST)
- communicates with JSON
- authenticates with a JSON Web Token (JWT)
this document will only focus on the packages/web-app
folder
Why no GraphQL? GraphQL seems to be a nice tech, but I simply didn’t have time to dig into it.
supported features & Tech
NOT USING CREATE-REACT-APP OR NEXT.JS
I made this universal application to learn more about React.
- I wanted to know how things work, so I didn’t use any frameworks like next.js or create-react-app that will build things for me that I don’t truly understand.
- I also wanted to make an exhaustive application: not a TODO app example.
There are plenty of those already, It’s good to begin with but whenever you want to build something more complex, you’ll have a hard time stitching the pieces together.
FEATURES
In order to make it the most real life example this web-app will:
- mutualise all the code we can
- support authentication
- support Internationalization (i18n)
- be testable (even if there isn’t as much tests as I wanted 😨)
- should work without JS in the browser
- I believe in progressive enhancement
- while developing, this allows us to make API POST request without taking care about the redux actions.
Those can be created in a second time. - I will use
browser cookie
to store the JWT.
It’s the only way to store informations on the browser without relying on Javascript.
Sadly a browser without JS & cookie is doomed 😔
TECH STACK
React library, among others, is a great way to ensure that our applications is perfectly in sync with our application state.
So we can rely on it to always render the proper thing depending on the route/user actions/API queries.
Thus, we will omit this part from this document (i.e. considering that changing the route/state will always render the right HTML)
Here are all the main modules used:
- views
- routing
- React router 4
- react-router-config 1 for the universal support
- application state
- redux 4
- redux thunk for a better handling of asynchronous actions
- react redux for a better integration with React
- server
files
STRUCTURE
I tried to avoid nesting folders too deeply.
I used lerna to have a clear separation between our API & the web-app.
I may move to yarn workspaces when it will leave its experimental status
Here are the main choices:
- client:
root
: a single file to initialize the Redux-store, the router and hydrate our React application
- server
root
: initializing our Koa app & the routingpublic
: all our compiled JS/CSS + some assets
- shared
root
- isomorphic files
- main HoC (will come to them later)
redux-ducks
: all our Redux related code using the ducks convention
This helps keeping all our Redux related code in one file[…components]
: organized by domain
Theui
are mostly presentational components
I could have used more external components
MUTUALIZATION
As for the version 1.1.0:
front | server | shared front/client |
---|---|---|
36 loc | 279 loc | 6476 loc |
1% | 4% | 95% |
I don’t expect this repartition to change much with futur versions.
There should be:
- more & more code into the shared folder
- some small additions in server code (mainly for proxying POST fallback)
building the applications
Using React with JSX makes the code easier to write and to maintain so:
- a building step is required to convert JSX to regular JS
- the most popular solution right now is the couple Webpack/Babel
- Webpack in version 4 since a while
It promises to be simpler, but you will still find yourself adding some plugins/loaders at one point or another - as the latest version of Ava use babel 7, I picked it for my build process also.
At this time (may 2018) it’s inbeta 47
😳 and working perfectly
I can’t thank enough all the people contributing to this project and I really hope that the final release will come soon
- Webpack in version 4 since a while
- since we have a build step, why not
- use ES2015 modules
- import our
scss
files directly in the components.
This is totally optional and could have been done in a classical way (like compiling a SCSS folder to a CSS file)
But I found that it really helps to isolate concerns about what your Component is about
Also it will make it easy to keep the styles next to the markup (no more back & forth from component folder to a scss folder)
- I don’t use
@babel/register
in my server code because it might have a performance cost so:- build also the server code with webpack
And that will also allow me to replace some files when needed - don’t build the code for tests
performance aren’t an issue there and we can use@babel/register
without worrying
- build also the server code with webpack
SERVER
- don’t want to bundle the
node_modules
: they are already accessible in nodeJS environment
→ done with webpack-node-externals - want to always have access of source-map
→ done with the the webpack banner-plugin and the source-map-support module - ignore
.scss
requires
→ done with babel-plugin-transform-require-ignore
CLIENT
- want to bundle the
node_modules
in a separate file
→ done with webpack split-chunks-plugin - want to bundle
.scss
in a.css
separate file
→ done with webpack extract-text-webpack-plugin
The@next version
is working fine withwebpack 4
but I should migrate to webpack mini-css-extract-plugin (here is why)
BUILD SUMMARY
PARCEL JS SIDE-NOTE
On a side node ParcelJs seems very promising.
As I see it, it’s still too young (version 1 released on december 2017).
I’ll wait a little bit for more documentation & tutorials, and surely try it on another side projet
sharing the configuration
I use to manage my server configuration with rc.
I wanted to keep it that way but an isomorphic configuration comes with some challenges.
To keep it versatile, I wanted to pass my configuration down to the client like this:
1 |
|
Unlike Viktor Turskyi’s solution, I replaced the config import with specific server/client files.
This prevents mixing ES modules with Node’s CommonJS modules syntax
→ done with Webpack’s normal-module-replacement-plugin
SERVER
1 |
|
CLIENT
1 |
|
where window.__CONFIG__
is passed by the server
DURING TEST
AVA is used for testing.
By default it uses babel to convert JSX. So I tried to keep it that way so → no Webpack.
This will make it easier to require a single component and test it.
So I just use my configuration’s entry point as the test configuration: no need to replace it with webpack!
I use the same babel configuration than the server’s one to prevent including the SCSS 😀
CONFIGURATION SUMMARY
application flow summary
This is how the app behaves from the first render made by the server to the subsequent client handling
Here is a little bit of explanation:
- symbols
- represents a cookie either read from a server request, or from the browser
- represents a JWT which will be used for authentication between our web-application and the API
- arrows between them represent reading/writing from/to the cookie
- REACT-ROUTER will mutualise all our pages routes
- on the server: direct call to the API (either in GET or POST) will be manually proxied
- this for supporting no-JS environment
- this is done in the
server/routing-api-backup.js
- REDUX will maintain our app state
- I uses the duck convention to organize the code
- API calls will be made in
redux actions
- ISO-FETCH is a small wrapper around isomorphic-fetch
It will handle any Fetch request to the API
Keep in mind that:- on the server: the cookie content will be provided by the server
- on the client: it can read the browser’s cookie content by itself
routing with React-Router & Redux
WHAT IS REACT-ROUTER
React-router is, I think, the most common routing solution for React.
They have recently updated their library to the version 4.
There is a huge shift of philosophy between the previous versions and this one.
They call it dynamic routing and it’s very different from the classical way.
INTERFACING WITH THE SERVER
To interface nicely with our Koa server, we need something that:
- is more traditional & plays well with a server routing
- can be easily shared between the server/client
For that they have made a package named react-router-config.
It’s still in beta but is already working as expected.
React Router Config mainly does 3 things:
- a way to define a routing configuration
- a method to retrieve the component that match the route
- give a way for the router to give back informations to the server (like not found & redirection) so we can serve the pages with right HTTP code.
GET REDUX ACTIONS FROM COMPONENTS
Like seen before, with react-router-config it’s easy to get which components to render.
But we need a way to tell our server which data those components need.
We will rely on Redux to maintain a coherent state.
What we need is redux actions that we dispatch to our store and redux will do his job.
But because it’s an universal application:
- on the server we will need the actions to be called before instantiating our components
→ this is solved by using a static method on our components - on the client we will need the actions to be called in componentDidMount()
- on first rendering we must prevent the client to call the componentDidMount() actions
Calling them twice won’t have a lot of side effects but making the same set of requests is inefficient…
The solution came again from Viktor Turskyi’s post about data fetching.
We need to make a HoC to take care of this.
It will:
- take as an input a
component
and an array of redux actions (actionsCreators
) - always add the authentication action (needed for the app to ensure the right display)
- return the
component
in therender()
, passing in anyprops
- for the server: expose a static method named
fetchData
which willdispatch
anyactions
of theactionsCreators
array - for the client: call
fetchData
incomponentDidMount
- prevent the first call of
componentDidMount
(with a module variable namedSKIP_FIRST_COMPONENTDIDMOUNT
)
LIMITATIONS
The main issue of doing so is that we need to call all the actions needed for all the children components in the top page component
It would be nicer to declare all those actions on the concerned components and find a way to hoist & aggregate them to the page component.
SERVER FLOW SUMMARY
isomorphic-fetch
One of the problem was to be able to send the JWT on any request.
- on the client we have access to the browser cookies at any time
→ no problems here - on the server we have access to the browser cookies only in Koa context
isomorphic-fetch
won’t be able to grab them on its own- we need to make possible to feed the JWT to
isomorphic-fetch
- we have to keep in mind that
isomorphic-fetch
can be called inredux-actions
so we need to pass the JWT in redux-actions also
FETCH SUMMARY
authentication
This one is quite easy.
Authentication is handled by 2 HoC:
- public route will redirect to private home if connected
→ done inauthentication-forbidden.js
- private route will redirect to login page if NOT connected
→ done inauthentication-required.js
They have the same requirements:
- be connected to the redux store to check authentication
→ done with react-redux - be connected to the react router to access the redirection
→ done with react-router-config
On the server we also a provide aserverContext
object
(on the documentation they call it staticContext but I find it more obvious to call it serverContext) - the Component to render if everything’s ok
And they will act in the same way:
- check
redux store
authentication status - handle the redirection if needed
on the server we will update updateserverContext
if a redirection happens.
This will help Koa to set the right HTTP status code when serving the page - OR render the Component if not redirection is necessary
AUTHENTICATION HOC FLOW
I18N with React-Intel
React-Intel fits my needs:
- formating numbers & prices
- formating dates
- providing translations
The documentation is quite good and the implementation straightforward.
We just need to:
- keep our current locale in the
Redux-Store
so we can change it dynamically - wrap our application with the
<IntlProvider />
component - define our
locales
files - follow the guide to server rendering
What we can improve:
This simple take is suitable for a small application but may be hard to maintain on a larger scale.
- load asynchronously our
locales
files- right now all
locales
are bundled into the different applications
- right now all
- have a way to extract our translation’s keys from the application
- a very interesting post was written by Vlad Goran about extracting those keys with babel-plugin-react-intl but it doesn’t seem to work with babel-7
adding React-Helmet
We still need to provide <head>
and <script>
tags.
In order to do so, and to keep most of the code on the shared folder, just use React-Helmet
It will handle for us:
- the
<html>
tag - the
<title>
tag - any
<meta>
and<stylesheet>
I didn’t put any <script>
for a reason that I can’t remember 😶
Since most of the HTML will be handled by React, on the server we won’t need to write a lot of tags, thus we can use Javascript template strings instead of a regular template engine.
the full chain of components
So from top to bottom this how our components fits together.
The main thing is that our HoC won’t change over time so we just have to write our application without worrying about server/client, auth, i18n anymore!