React SSR: initial data fetching & hydrate

試想你用 React 刻了一個論壇的 Single Page App,但是為了 SEO / Performance 考量,你決定第一畫面的文章列表在 server side 先 render 出來,這時該怎麼做?

server-side fetching data from reddit endpoint

寫在文前

Step 0— File structure

├── client.js #主要是 hydrate logic
├── package.json
├── server.js
└── src
└── index.js

Step 1—Server-side fetch data

// src/index.js
const endpoint = "https://iamendpoint/v1/getPosts";
const App = () => {
// call endpoint
// and do pagination w/ initial data if trigger bottom
return (
<ul>
{data.map({title,author}=>
<li key={title}>{author}/{title}</li>)
}
</ul>
)
}
// src/index.js
const endpoint = "https://iamendpoint/v1/getPosts";
const App = ({initialData={}}) => {
// do pagination w/ initial data when trigger bottom
return (
<ul>
{initialData.map({title,author}=>
<li key={title}>{author}/{title}</li>)
}
</ul>
)
}
App.getInitialData = async ({req})=> {
const response = await fetch(endpoint, {headers: req.headers});
const initialData = await response.json();
return {initialData};
}

Step 2— call getInitialData() before renderToString

// server.js
import App from './src/components/App.jsx'
router.get('/', async(req,res)=>{
const ReactAppHTML = ReactDOM.renderToString(<App/>);
const html = `
<div id="root">{ReactAppHTML}</div>
<script src="client.js"></script>
`
//我懶得寫 html body tag
res.status(200).send(html)
})
router.get('/', async(req,res)=>{
const {getInitialData} = App;
const props = await getInitialData();
const ReactAppHTML = ReactDOM.renderToString(<App {...props}/>)
//下略
})

Step 3—hydrate 階段也要有 state

Photo by Jacek Dylag on Unsplash
// client.js
import React from 'react';
import {hydrate} from 'react-dom';
import App from './components/App.jsx'
hydrate(<App/>,document.querySelector('#root'));
明明 server side generate 好的東西,使用者卻又要再重打一次,在SEO/Performance audit 上看來絕對是顯著的 Cumulative Layout Shift impact。
router.get('/', async(req,res)=>{
const {getInitialData} = App;
const props = await getInitialData();
// 這邊拿到 data 惹對吧
const ReactAppHTML = ReactDOM.renderToString(<App/ {...props}>) const html = `
<script>
window.__my__fucking__props__=${JSON.stringify(props)};
</script>
<div id="root">{ReactAppHTML}</div>
<script src="client.js"></script>
`
// again 我懶得寫 html body tag
//下略
})
// client.js
import App from './src/index.jsx'
hydrate(
<App {...window.__my__fucking__props__}/>,
document.querySelector('#root')
);
react events / hooks testing
curl https://react-ssr-reddit-meme.herokuapp.com/ testing

尾聲

If any interest, 👉 https://realdennis.me.

If any interest, 👉 https://realdennis.me.