How to Implement SSR and Client Hydration in Next.js
Data Assembly and Update Mechanism in SSR and Hydration
This article explains the workflow of server-side rendering (SSR) and client-side hydration in Next.js. It covers the process of server-side data injection, client-side hydration, and mechanisms to ensure timely updates to data (e.g., self.__next_f
). Additionally, we discuss handling scenarios where page.js
is loaded before the data.
1. Workflow of Server-Side Rendering (SSR)
a) Server Obtains Data
In SSR frameworks like Next.js, data required by the page is fetched server-side, typically using getServerSideProps
or getStaticProps
.
Example:
1 export async function getServerSideProps() {2 const res = await fetch('https://api.example.com/data');3 const data = await res.json();4 return { props: { data } };5 }
b) Injecting Data into HTML
Once data is fetched, it is injected into the HTML, commonly using:
- Script tags: Serialize the data into JSON and embed it.
- Global variables: Assign data to global variables like
self.__next_f
.
c) Send HTML to the Client
The HTML, containing the data, is sent to the client.
d) Client Hydration
On the client side, React and the page JavaScript extract data injected by the server and "activate" the static HTML using ReactDOM.hydrate
.
2. Implementation Details
a) Server-Side Data Injection
Data is serialized into JSON and embedded in an HTML <script>
tag.
Example:
1 import ReactDOMServer from 'react-dom/server';23 function App({ data }) {4 return <div>{data.message}</div>;5 }67 export async function getServerSideProps() {8 const data = { message: 'Hello, world!' };9 return { props: { data } };10 }1112 export function renderToStringWithData(App, props) {13 const html = ReactDOMServer.renderToString(<App {...props} />);14 const dataScript = `<script>self.__next_f = ${JSON.stringify(props)};</script>`;15 return `16 <!DOCTYPE html>17 <html>18 <head><title>SSR Example</title></head>19 <body>20 <div id="root">${html}</div>21 ${dataScript}22 <script src="/client.js"></script>23 </body>24 </html>25 `;26 }
b) Client Hydration
The client extracts data from self.__next_f
and activates the page.
Example:
1 import React from 'react';2 import ReactDOM from 'react-dom';3 import App from './App';45 const data = self.__next_f;6 ReactDOM.hydrate(<App {...data} />, document.getElementById('root'));
3. Ensuring Timely Updates to self.__next_f
a) Using Proxy to Monitor Changes
A Proxy
object can track changes to self.__next_f
and trigger hydration when data updates.
Example:
1 self.__next_f = self.__next_f || [];23 self.__next_f = new Proxy(self.__next_f, {4 set(target, prop, value) {5 const result = Reflect.set(target, prop, value);6 if (prop === 'length' && value > 0) {7 console.log('Data updated:', target);8 hydrateApp(target);9 }10 return result;11 }12 });1314 function hydrateApp(data) {15 const parsedData = JSON.parse(data[0][1]);16 ReactDOM.hydrate(<App data={parsedData} />, document.getElementById('root'));17 }
b) Monitoring Data in page.js
The script waits for data availability before proceeding.
Example:
1 function waitForData() {2 return new Promise((resolve) => {3 if (self.__next_f && self.__next_f.length > 0) {4 resolve();5 } else {6 const observer = new MutationObserver(() => {7 if (self.__next_f && self.__next_f.length > 0) {8 observer.disconnect();9 resolve();10 }11 });12 observer.observe(document.body, { childList: true, subtree: true });13 }14 });15 }1617 waitForData().then(() => {18 const data = JSON.parse(self.__next_f[0][1]);19 ReactDOM.hydrate(<App data={data} />, document.getElementById('root'));20 });
4. Handling page.js
Loading Before Data
a) Initialize self.__next_f
Add an initialization script in the HTML header:
1 <script>2 self.__next_f = self.__next_f || [];3 </script>
b) Embed Data Post-HTML
Embed data after the HTML:
1 <script>2 self.__next_f.push([1, "{\"name\":\"The Octocat\"}"]);3 </script>
Conclusion
By implementing these strategies, you can:
- Ensure server-side data is correctly injected into HTML.
- Facilitate timely client-side hydration.
- Handle cases where
page.js
loads before data.
These practices enhance the reliability and efficiency of SSR and hydration in Next.js.