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 🏃♀️
You should have some notions with:
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 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
Why no GraphQL? GraphQL seems to be a nice tech, but I simply didn’t have time to dig into it.
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.
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 cookieto store the JWT.
Sadly a browser without JS & cookie is doomed 😔
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:
- application state
Here are the main choices:
root: a single file to initialize the Redux-store, the router and hydrate our React application
root: initializing our Koa app & the routing
public: all our compiled JS/CSS + some assets
- 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
uiare mostly presentational components
I could have used more external components
As for the version 1.1.0:
|36 loc||279 loc||6476 loc|
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)
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 in
beta 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
scssfiles 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/registerin 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
- build also the server code with webpack
- 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
→ done with babel-plugin-transform-require-ignore
- want to bundle the
node_modulesin a separate file
→ done with webpack split-chunks-plugin
- want to bundle
→ done with webpack extract-text-webpack-plugin
@next versionis working fine with
webpack 4but I should migrate to webpack mini-css-extract-plugin (here is why)
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
To keep it versatile, I wanted to pass my configuration down to the client like this:
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
window.__CONFIG__ is passed by the server
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 😀
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:
- 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
- REDUX will maintain our app state
- I uses the duck convention to organize the code
- API calls will be made in
- 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
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.
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.
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.
- take as an input a
componentand an array of redux actions (
- always add the authentication action (needed for the app to ensure the right display)
- return the
render(), passing in any
- for the server: expose a static method named
- for the client: call
- prevent the first call of
componentDidMount(with a module variable named
The main issue of doing so is that we need to call all the actions needed for all the children components in the top
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.
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-fetchwon’t be able to grab them on its own
- we need to make possible to feed the JWT to
- we have to keep in mind that
isomorphic-fetchcan be called in
redux-actionsso we need to pass the JWT in redux-actions also
This one is quite easy.
Authentication is handled by 2 HoC:
- public route will redirect to private home if connected
→ done in
- private route will redirect to login page if NOT connected
→ done in
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 a
(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:
redux storeauthentication status
- handle the redirection if needed
on the server we will update update
serverContextif 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
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-Storeso we can change it dynamically
- wrap our application with the
- define our
- 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
- right now all
localesare bundled into the different applications
- right now all
- have a way to extract our translation’s keys from the application
We still need to provide
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:
I didn’t put any
<script> for a reason that I can’t remember 😶
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!