React Hydration explained
Saeed Mosavat
Posted on June 28, 2024
If you are a frontend developer and have some experience with React, you may have heard the term "Hydration". In this article we will explore what it is, why we need it and how it works.
This is what the official react documentation says about hydration:
In React, โhydrationโ is how React โattachesโ to existing HTML that was already rendered by React on a server environment. During hydration, React will attempt to attach event listeners to the existing markup and take over rendering the app on the client.
"React on the server", you may wonder! "Wasn't react a client-side thing?"
Well, not anymore. This is 2024 and we even have Server Components now (how crazy is that?!)
There is a lot of things going on in this definition that we will cover in this post. Let's start with why and how react is rendered on the server.
React on the server
Traditionally, a react application is a tiny HTML with a giant JavaScript including react code itself and the application code that uses react. when this JavaScript runs, it will populate the HTML with some meaningful and interactive content (our app).
<html>
<head>
<title>React App</title>
<script src="the/giant/app.bundle.js" ></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
We can send this tiny HTML to the users, their browser will download and run the JavaScript code, which will populate that "root" div element and then they'll have the running application. No matter what page the user is looking for, it can always start from the same HTML above. This is actually how Create React App works.
There are two things to consider:
Every time a user opens our website, they will see a blank page followed by a spinner for a while. The blank page is not surprising because we have sent them an empty HTML! But even after receiving that HTML, it takes some time to download the js files (still showing a blank page) and even when js files are downloaded and our application runs, it usually needs some data from the server to show the page content, so we show spinners while waiting for that data. It kinda sucks, right?
Humans are not the only users of our websites, robots are visiting too. Like the Google Bot.
But unfortunately, robots have more work to do than humans and they will not wait for our stupid spinners to finish. (Well actually sometimes they do, but they do it later in a "less busy" time and we don't want to rely on that).
So, in order to keep both humans and robots happy about our website, we should give them the real HTML they are looking for the moment they request a page. Just like the good old-fashioned websites. And since our application is rendered by react, we need a way to "pre-render" the app on the server to get that HTML.
For simplicity let's imagine our App only has one page (so we can ignore "routing" on the server) and this is the corresponding component:
const App = () => {
const [product, setProduct] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch("/product")
.then((res) => res.json())
.then((data) => setProduct(data))
.finally(() => setIsLoading(false));
}, []);
if (isLoading) {
return <div>Loading...</div>;
}
return <div>{product.name}</div>;
};
So, we need to run our react App on the server to get the final HTML that the user wants to see and then return that HTML as the response to the clients request for the page.
React provides a renderToString
method specifically for this purpose. All we have to do is to pass it the JSX element for the root of our App and get the rendered app as HTML. then we can send back this HTML to the client:
import { renderToString } from 'react-dom/server';
const html = renderToString(<App />);
But, there is a small problem. Our app calls an asynchronous API to fetch the data and render something meaningful. However, there is no way for react's renderToString
method to know this logic and to know how much it should wait for the App to finish all data fetches and re-renders until the final meaningful content is rendered. So it just lets the app to render once and before executing any useEffects
or re-renders, it returns the resulting HTML. Oops!
In our example the result will be something like this:
<html>
<body>
<div id="root">
<div>Loading...</div>
</div>
</body>
</html>
This is obviously not desired. The solution to this is to provide the data that is needed for the App at the time we want to render the App. So we have to make some slight improvements:
const App = (props) => {
const [product, setProduct] = useState(props.initialProduct);
const isInitialized = !!props.initialProduct;
const [isLoading, setIsLoading] = useState(!isInitialized);
useEffect(() => {
if(!isInitialized) {
fetch("/product")
.then((res) => res.json())
.then((data) => setProduct(data))
.finally(() => setIsLoading(false));
}
}, [isInitialized]);
if (isLoading) {
return <div>Loading...</div>;
}
return <div>{product.name}</div>;
};
const response = await fetch("/product");
const data = await response.json();
const html = renderToString(<App initialProduct={data}/>);
And now we get the correct HTML that shows the product details.
Back to the client
So far the client has received the content-full HTML, along with the js files to run the react app. Robots are happy now, but it's not the case for our human users because React is not happy! When React loads, it tries to render the <App>
and create something called "Virtual DOM" (just think of it as a copy of the page document, managed by react). Virtual DOM is the single source of truth for React and all its changes will be reflected on the browser DOM. so it has to make sure the browser DOM matches the virtual DOM to begin with. But in our example, react app has started with a loading state in the first render on the client (because here we don't have any value to pass to <App>
as initialPrduct
prop) while the browser DOM has the product details. React has to get rid of anything that it doesn't acknowledge in the document, so it removes the page and shows the loading state again! We are back to square one!
Dehydration to rescue
To address the above issue and prevent a loading state on the client and keeping the already fetched data on the page, we should somehow transfer the fetched data on the server to the client. This way the client has everything it needs to take over the rendering of the App that has started on the server.
What if We could send the server data in a JSON string alongside the rendered HTML. Let's assume the server has put this script in the HTML:
<script>
window._data = '{ "id": 1, "name": "some product name" }';
</script>
We can then use it to bootstrap the app like this:
// index.jsx
const dataFromServer = JSON.parse(window._data);
ReactDOM.render(
<App initialProduct={dataFromServer} />
document.getElementById("root")
);
This process of sending the data required for hydration from server to client is called dehydrarion. Frameworks like Next.js handle this process seamlessly for you. You can inspect the window.__NEXT_DATA__.props
object in the console of a Next.js website to see it in action.
Conclusion
In summary, hydration is a crucial process in React applications that allows for a seamless transition between server-rendered HTML and the fully interactive client-side application. By sending the necessary data alongside the initial HTML, we can avoid an unnecessary loading state on the client and ensure a smooth user experience. This approach improves initial load times, enhances SEO, and provides a more responsive application for users.
Posted on June 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 3, 2024
August 26, 2024