Written by Rafael Quintanilha✏️
If you have been developing Gatsby applications recently, it is likely that this has bitten you at some point.
Consider the following scenario: your application stores some information in the client (the user’s browser), possibly in the Local Storage.
The goal is straightforward — you want to quickly make UI changes based on individual data (e.g. theme preference, log-in details, etc).
You set everything up properly and it works locally, but as soon as you do the deploy things don’t look quite like what you expected.
What happened?
This is what we call a rehydration issue.
In other words, React and Gatsby are having trouble matching data that came from the server with the change you want to do in the client.
This happens because React does not expect any mismatches between what was statically generated in the server and what was rendered by the client.
Before we dive deeper into what is going on, let’s first go over how Gatsby works.
Server side rendering
Gatsby has a magnificent set of server side rendering (SSR) features.
That means that it is able to render complex pages in the server and send the full output (a compiled file) to the client (your browser).
This contrasts with regular React applications (such as the ones created by Create React App), which are completely generated in the client, i.e., everything you see on the page is dynamically created after the JavaScript is downloaded, parsed, and evaluated.
With SSR, you already send the rendered content.
The end result is that your page loads really fast.
Cool, huh?
In a typical client-server exchange in an application powered by Gatsby, the page is retrieved first and the JavaScript is evaluated later.
It allows for quicker page loads, TTI values, faster first meaningful paint, and several other performance metrics. By the time your JavaScript is ready to come into play, a lot has already happened on your user’s screen. Magic!
However, as you may have guessed, this has a caveat — if your page is server side rendered, but you need information that is available only in the client to complete the UI, you have a lacuna.
Suppose that you need to display a login button for a visitor and a logout button for a user.
If the information that the user is logged in (for example, a token stored in the Local Storage) is in the client only, how can the server side rendered page knows which button to display before the JavaScript loads?
In fact, if the user is already logged in, you will experience an annoying event — the page will first display the login button (for there is no token available in the server whatsoever), and then it will revert to the logout button (as soon as the JavaScript code is evaluated and the token retrieved from the Local Storage).
This is annoying, but not the end of the world. It gets worse, though.
Suppose that the two buttons are in the same DOM tree and are conditionally rendered based on the information stored in the Local Storage.
Suppose also that they have different colors.
It is possible that their styles will be mixed up and you will end up with different colors than expected!
This is the end consequence of the so-called rehydration issue, where React will have trouble rehydrating the existing server side rendered DOM with the dynamic content generated by the client.
This is a problem that has been troubling Gatsby’s users for a while now. Fortunately, there is a simple way to overcome this problem and we’ll explore it.
The rationale
As we’ve seen, this problem occurs because there is a mismatch between what the server renders and what the client generates.
We can mitigate this issue by holding off the particular element from rendering until we know for sure the client has fully loaded.
Bottom line is: why request the server to render if you can only guarantee the integrity after the client has acted?
Of course you are looking to smaller pieces of UI, not a whole page, which would contradict the whole point of having SSR in the first place.
In fact, if you see yourself having this dilemma for a big chunk of your page, chances are you want to make it a client-only page instead of serving it directly from the server.
Deferring to the client
Now that we understand that we don’t need to render the element until the client has loaded, how can we implement it? Fortunately, there is a very elegant way of doing it with React Hooks.
import React, { useState, useEffect } from 'react';
const Component = () => {
const [isClient, setClient] = useState(false);
useEffect(() => {
setClient(true);
}, []);
return <div>I am in the {isClient ? "client" : "server"}</div>;
}
The above Hook leverages useEffect
in order to correctly detect whether the user is in the server or in the client.
The logic is as follows — useEffect
with an empty dependency array runs only after the first render is committed to the screen (you can read more about it in the documentation).
If you are in the server, there is no screen at all. As a result, the effect never runs and isClient
will be always false
.
On the other hand, for any application rendered in the client, after the first render the effect will run and isClient
will become true
.
This simple snippet is enough for us to detect where the code is being run.
However, it doesn’t solve the potential issue with rehydration.
Consider the component below:
/* Component.module.css */
.red {
color: red;
}
.blue {
color: blue;
}
const Component = () => {
const [token, setToken] = useLocalStorage('token', "");
const isLoggedIn = token !== "";
/* Generate some fake token */
const onLogin = () => setToken(Math.random().toString(36).substring(2));
const onLogout = () => setToken("");
return (
<div>
{isLoggedIn
? <button className={css['red']} onClick={onLogout}>Logout</button>
: <button className={css['blue']} onClick={onLogin}>Login</button>}
</div>
);
}}
Now we are retrieving a token from the Local Storage (you can check how to do it with a Hooks approach here) and rendering different components based on that information.
Due to the hydration issue, when the server first delivers the page to you, if you are logged in, you are going to see a blue Logout button!
Good luck debugging why this simple code isn’t working only in production.
Drama aside, now that we identified the problem, how can we solve it?
Well, the key is to use the key
prop of the component.
In simpler terms, key
tells React that whenever it changes, the component needs to re-render.
The updated version of the code becomes:
/* Component.module.css */
.red {
color: red;
}
.blue {
color: blue;
}
const Component = () => {
const [token, setToken] = useLocalStorage('token', "");
const isLoggedIn = token !== "";
/* Generate some fake token */
const onLogin = () => setToken(Math.random().toString(36).substring(2));
const onLogout = () => setToken("");
return (
<div>
{isLoggedIn
? <button className={css['red']} onClick={onLogout}>Logout</button>
: <button className={css['blue']} onClick={onLogin}>Login</button>}
</div>
);
}}
Note that key
changes when the component detects it now runs on the client, forcing the element to re-render and apply the correct style (adapted from this comprehensive reply gave by Dustin Schau in GitHub.)
Yet this only solves half of the problem.
Our styling mismatch issue was fixed, but there is still an annoying flickering due to the fact that the server renders first and then the client comes in and does its change.
Unfortunately, there is no way of fixing it without resorting to somehow letting the server knows beforehand if the user is logged in or not. However, a simple approach is to prevent the element from rendering at all whenever you detect that it is fully usable only in the client.
import { useState, useEffect } from "react";
const useIsClient = () => {
const [isClient, setClient] = useState(false);
const key = isClient ? "client" : "server";
useEffect(() => {
setClient(true);
}, []);
return { isClient, key };
};
export default useIsClient;
Hooks version
Adding the above logic to every component that suffers from rehydration can be very cumbersome.
Thankfully, there is a way to simplify the code with a straightforward Hook:
const Component = () => {
const [isClient, setClient] = useState(false);
const key = isClient ? "client" : "server";
const [token, setToken] = useLocalStorage('token', "");
const isLoggedIn = token !== "";
/* Generate some fake token */
const onLogin = () => setToken(Math.random().toString(36).substring(2));
const onLogout = () => setToken("");
useEffect(() => {
setClient(true);
}, []);
if ( !isClient ) return null;
return (
<div key={key}>
{isLoggedIn
? <button className={css['red']} onClick={onLogout}>Logout</button>
: <button className={css['blue']} onClick={onLogin}>Login</button>}
</div>
);
}
Which looks way cleaner and is ready to be reused across your codebase!
Visualizing the issue
The rehydration problem and its workarounds can be easily observed in the following gif:
The first button has the rehydration problem. Notice how it displays the wrong color after we reload the page (i.e., when we ask the server to send the HTML).
The second button fixes the issue. However, you’ll notice it first displays the blue Login button.
Finally, the last button does not show up until the JavaScript is fully evaluated.
The last two effects are more noticeable in slow connections. Consider the following simulation of a user within a 3G network:
The code for the above example is available in GitHub.
There’s also a live version if you want to check for yourself.
Conclusion
Server side rendering is a powerful concept and it really makes your application fast.
Gatsby knows how to best leverage it in order to enhance performance.
However, you need to be mindful when client and server need to talk. The main takeaways from this piece are:
- Use the
key
attribute whenever your component changes depending on what it is being rendered - Prevent the component from rendering until you acknowledge that the application is running in the client to avoid flicker
- Adopt
useIsClient
in order to reuse the necessary logic for accomplishing the two points above
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post Fixing Gatsby’s rehydration issue appeared first on LogRocket Blog.
Top comments (0)