Nearly two years ago, I started using React and I’ve always been curious about doing server-side rendering, but never bothered. Partly because the largest application I was working on had most of its content behind a login and didn’t need massive SEO on every page, but also because it seemed like a huge hassle and the tools were, let’s say, “evolving,” very quickly.
roast.io is a frontend host which renders your pages using headless Chrome before deploying to a CDN. In other words, it does all the server-side rendering for you without having to have a “frontend server” that will know how to do the whole, “preload your root component, inject it with props, render to string, inject in a template, etc.” Given that it’s a CDN, (i.e. it’s a cache!) it works great for pages that need SEO, but don’t need login. So it won’t solve all server-side rendering problems, but does solve some of the high-value ones.
Ok, ok, so it’s not capital-S Serverless in the sense of small computing functions, but it is serverless in the sense of I don’t have to run a server. This is helpful for me who just wants to play around with server-side rendering, but awesome for people who have single-page apps for whom it would be really difficult to run a node server for one reason or another.
So I set about creating a project to deploy roast.io and thought I’d share my experience. Spoiler: it was incredibly easy and took 0–10 lines of code to hook up server-side rendering. Besides loading faster, I also changed the number of API calls from O(n) to O(1) in the number of client requests. I explored using component state, Redux, and MobX, so I’d see how different state management schemes worked with roast.io. Without further ado, let’s check out the app…
Disclaimer: Jonathan, the creator of roast.io, is a friend of mine. He asked me to try out roast.io and blog about it, but he didn’t pay me. He did offer to buy me coffee the last time we hung out, but it was too late in the day for me to be having caffeine. Instead I jumped at the opportunity to make an article with incredibly high buzzword density in the title and here we all are.
To get the full benefit of roast.io, I’d need an API to load some data from. Looking through this awesome list of public APIs, I found the Makeup API which was perfect — it’s open with lots of detail in JSON and links to images. I set about making a pretty straightforward React app that displays the makeup products by brand. (My favorite data from the API was that it had hex codes for many of the products’ different color options. That was a fun React component. 💅)
Ok, so the makeup is a lot better looking than my app, but it should serve to get us going with testing out the server-side rendering. You can go check out the source of the app and the deployed app, but you’ll find it mostly just looks like a (rough) React app. In other words, I got to be lazy and do practically nothing in getting the server-side render to work. I’ll describe what I did do below.
First I tried just loading data in by brand using component state because the app’s not too complicated. In other words, when a brand was loaded by React Router, the Brand component looked at the path, requested the data from the API for that brand, then saved it locally using setState().
When I deployed, I needed to create a little file called ssr.json with a list of the paths I wanted roast.io to render on the server. Many apps probably won’t need this because roast.io will crawl your site, but I had to be special and make an app without a default landing page. Fortunately roast.io has an out for snowflakes like me. ❄️
Other than that, I did… nothing. The app was deployed and all the markup was rendered on first download. Huzzah!
The only problem with this approach is that even though the page would load quickly with all the markup rendered, it would still make an additional fetch when each user visited because the app had no way of knowing that it was server-side rendered. While we could deal with this problem using component state, the Redux community in particular has a lot of documentation on doing “rehydration” of state. So I tried converting the app to Redux…
Using the Redux “ducks” style, I created a reducer with actions and action creators as shown below. (You can skim the code, I summarize the important bits after.)
I used redux-thunk for asynchrony here since we just have one call. The basic fetch is the same, but there are some notable changes:
- It’s Redux-ified, i.e. it does no modification of any structures in place and the reducer integrates everything into a state tree.
- The state tree holds all the data from any brand we encounter so switching back to a brand we’ve already seen will be faster.
- The component now checks to see if the brand it’s rendering is available in the state tree and only fetches if it’s not.
- The state tree also holds an “in-progress” list of brands so we could display a spinner if we wanted to and so we don’t fetch a brand already in progress.
Most importantly though, notice that we’re grabbing the initial Redux state from PRELOADEDSTATE if it’s available. roast.io uses this variable as a handle for both saving and loading preloaded state if your app uses it. Here in the reducer we’re using it opportunistically, but falling back to empty state if it’s not there.
If it was available and had the data for the brand we’re trying to render available, the component wouldn’t fetch. It would just use what was in the preloaded state. Moreover, if the markup was already rendered, React wouldn’t need to change the DOM as it would realize it’s just rendering the same thing as was server-side rendered by roast.io because the checksums are intact on the cached page.
So how does it get there? Well, since roast.io just serializes whatever PRELOADEDSTATE points to at the end of its server rendering phase, we can set PRELOADEDSTATE equal to the state every time the reducer updates it. Redux gives us an excellent middleware framework to do this relatively easily.
So if we set up our store with the middleware above attached, roast.io will just save the entire Redux store and “dehydrate” it into the cached page. When we create our store again, we’ll just “rehydrate” it from PRELOADEDSTATE.
To recap, when the page loads, the markup is already in place so the browser can start rendering immediately and the state of the Redux store will be loaded so there’s no API call on the first page view. We’re using PRELOADEDSTATE opportunistically so we don’t have to do anything different with in our development environment versus our roast.io deployment. (Also if you ever want to get off of roast.io, you won’t have to make any code changes, but don’t tell Jonathan I said that.)
This page only ever requires 1 API call ever for any number of clients arriving via a search.
With that, this page only ever requires 1 API call ever for any number of clients arriving via a search. While the API in this example is free, things like Firebase and your own self-hosted APIs are not, so this architecture could save you quite a bit on your backend costs.
While Redux was my first venture into React state, I’ve started looking into MobX recently too. I knew Redux was had a lot of infrastructure set up for using preloaded state, but I was less familiar with MobX’s capabilities for server-side rendering. I was pretty amazed to discover that not only is it really easy to do, it works incredibly well with roast.io’s PRELOADEDSTATE variable setting approach. I converted the project to use MobX on this branch.
If you’re not familiar with MobX, the main difference you need to know for this article is that it modifies state in-place and publishes changes using a reactive architecture. The in-place thing is the important thing. Here’s what our state looks like for the makeup app:
Other than the special decorators, it’s really just an ES6 class. The data we’re keeping is the same as with the Redux approach, just in an ES6 Set and MobX observable map (which is pretty much just like an ES6 Map for our purposes). On initialization, we take some preloaded state and set it up to be observable for MobX, doing a bit of custom work, but not a ton. This class is analogous to what we did with the Redux reducer above.
So how do we get the preloaded state to save and load? Because the store is modified in place, we only have to set PRELOADEDSTATE once at creation.
That’s it! 🎉 We don’t even have to update PRELOADED_STATE with each action. And we get all the same benefits of server-side rendered markup and no API call.
So I don’t know if this experience counts as me doing setting up server-side render, but I got server-side rendering, so I’m going to count it as a win. So give it a try — there’s a free tier and all you have to do to get started is npm install -g roast, then run roast deploy.
I’m hoping to try Vue soon and see how this works out. If anybody gives it a try with roast.io, please let me know in a reply!
Did you know why Medium uses green hearts 💚 instead of red or another color? Me neither, but I really appreciate when people who like my posts click the little 💚. Follows are great too. :) Thanks!