Welcome, if you don’t already know, I wrote two posts on Server Side Rendering with Create React App, check them out here:
- https://hackernoon.com/server-side-rendering-with-create-react-app-1faf5a9d1eff
- https://medium.com/@benlu/ssr-with-create-react-app-v2-1b8b520681d9
This is a v2.1 because we’re not proposing a breaking change, where the v2 was a complete rewrite due to react-router v4
I’ve been a bit overwhelmed by the response I’ve gotten, and given the pull requests and questions I’ve gotten, I wanted to create an update. I don’t want to reiterate what’s in the other articles, but lets tackle a couple of issues with what we’ve done.
- Synchronous Data
- Images
- Basic build for production use
Synchronous Data is when you load your react page, with data for rendering already inside, and is synchronously sent to the client. The alternative is making an API call when your JavaScript has loaded.
Create React App loads images in a certain way, with cache busting built in. My past implementation simply ignored images, but that’s hardly ideal, so lets see how to add image support.
Production use involves some of the decisions and changes that you have to make in putting in to production, especially as babel-register is definitely not recommended for production work.
All the code I’m going to use is here: https://github.com/ayroblu/ssr-cra-v2.1 if you prefer to learn my messing around in the code, check it out!
Synchronous Data
Remember: you’re only sending html, everything else is async
You don’t alter your bundle, because its static and optimised.
You can inject data straight into your page
<script>
window.DATA = "{{data}}"
</script>
Server Render Function
Obviously, you do have the option to load in data, while the JS is loading, however, this is probably the best way, as its easy to do and pretty efficient, without the need for anything complicated. I might want to zip it, but the html sent over is already gzipped, so it doesn’t really matter.
For our server render function, we need to update it, the basic idea’s being:
- Server context, this is how we know its server rendered, and exposes server components to the frontend. This includes database APIs and request information, and placeholders for our finished data
- We render once, then wait for the data promises to load
- We render again, filling in with those resolved promises
- We fill in the built page with our app body, head and JS data that has been rendered and send that to the client
Here’s some code that pretty much does that, its pretty self explanatory, though references a few external functions:
Client Render Function
Now with this, we have some complicated logic that happens in our components, we need to handle the 4 states
- Server Rendered, first (no data)
- Server Rendered, second (has data)
- Client Rendered, first (no data)
- Client Rendered, second (has to fetch data) (and subsequent page changes)
That code ends up looking like so:
So a few things to note here, our _getData
could probably happen in our action or similar, also, our database API and our server served API are the same, you may want to seperate these. We can also add and change things like the head or status code.
Here’s an example where we return a 404 when we don’t match a route:
Images
This is where you start to have to do things more manually, currently with the current webpack configuration, images are inlined if they are smaller than 10,000 bytes, otherwise they’re given in the form: /static/media/file-name.[md5hash.8].suffix
So we need to add a custom function using ignore-styles
which does most of this for us! https://github.com/bkonkle/ignore-styles
So specifically for images here, if they’re smaller than 10000 bytes, we inline them, and if they’re greater, we add the correct static media url.
Note, we haven’t tried to handle multiple images with the same name, I haven’t reviewed its behaviour, but its probably not hard, so feel free to write a pull request for that, or at least let me know so it can handle that.
Production use
This means many things to many people, but for our purposes, we have one goal: to not require babel-register
.
I had trouble babelifying this before, but it turned out pretty easy, you’ll need to add a .babelrc
, whether to your package.json
or to your directory like so:
{
...
"babel": {
"presets": [
"env",
"react-app"
]
}
}
Add a npm script for babel, needs a NODE_ENV
, and we’ll ignore tests:
"babelify": "NODE_ENV=production babel src --out-dir babel-src --copy-files --ignore '**/*.test.js'",
Run it and you’re good to go!
Check out my code here: https://github.com/ayroblu/ssr-cra-v2.1 and an online demo here (try throttling and offline modes): https://ssr-cra-v21.now.sh/
Future Ideas
- Performance optimisation for
renderToString
, which is extremely slow (highly recommend caching all public routes by url) - Simplification: in the same spirit as Create React App, there’s a lot of refactoring and removal, that can hide or simplify the development
- Testing: I have no tests, I’m sorry??? Probably could add some basic ones
- Webpack? Other tools? Give a recommendation :)