renderToReadableStream は React ツγƒͺγƒΌγ‚’θͺ­γΏε–γ‚Šε―能γͺ Web Stream にレンダーします。

const stream = await renderToReadableStream(reactNode, options?)

補袳

こ�? API は Web Stream γ«δΎε­˜γ—γ¦γ„γΎγ™γ€‚Node.js γ§γ―γ€δ»£γ‚γ‚Šγ« renderToPipeableStream を使用してください。


γƒͺフゑレンス

renderToReadableStream(reactNode, options?)

renderToReadableStream を呼び出して、React ツγƒͺγƒΌγ‚’ HTML としてθͺ­γΏε–γ‚Šε―能γͺ Web Stream にレンダーします。

import { renderToReadableStream } from 'react-dom/server';

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆε΄γ§γ―γ€γ“οΏ½?γ‚ˆγ†γ«γ‚΅γƒΌγƒη”Ÿζˆγ•γ‚ŒγŸ HTML γ‚’ζ“δ½œε―θƒ½γ«γ™γ‚‹γŸγ‚γ« hydrateRoot を用います。

さらに例を見る

εΌ•ζ•°

  • reactNode: HTML γΈγ¨γƒ¬γƒ³γƒ€γƒΌγ—γŸγ„ React γƒŽγƒΌγƒ‰γ€‚δΎ‹γˆγ°γ€<App /> οΏ½?γ‚ˆγ†γͺ JSX θ¦η΄ γ§γ™γ€‚γ“γ‚Œγ―γƒ‰γ‚­γƒ₯γƒ‘γƒ³γƒˆε…¨δ½“γ‚’θ‘¨γ™γ“γ¨γŒζœŸεΎ…γ•γ‚Œγ¦γ„γ‚‹γŸγ‚γ€App γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ― <html> γ‚Ώγ‚°γ‚’γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚

  • 省η•₯可能 options: γ‚ΉγƒˆγƒͺγƒΌγƒ ι–’ι€£οΏ½?γ‚ͺγƒ—γ‚·γƒ§γƒ³γŒε«γΎγ‚ŒγŸγ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆγ€‚

    • 省η•₯可能 bootstrapScriptContent: ζŒ‡οΏ½?οΏ½γ•γ‚ŒγŸε ΄εˆγ€γ“οΏ½?ζ–‡ε­—εˆ—γŒγ‚€γƒ³γƒ©γ‚€γƒ³οΏ½? <script> タグ内に配�?γ•γ‚ŒγΎγ™γ€‚
    • 省η•₯可能 bootstrapScripts: γƒšγƒΌγ‚ΈδΈŠγ«ε‡ΊεŠ›γ™γ‚‹ <script> γ‚Ώγ‚°γ«ε―ΎεΏœγ™γ‚‹ URL ζ–‡ε­—εˆ—οΏ½?ι…εˆ—γ€‚γ“γ‚Œγ‚’δ½Ώη”¨γ—γ¦γ€hydrateRoot を呼び出す <script> γ‚’ε«γ‚γΎγ™γ€‚γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ§ React γ‚’γΎγ£γŸγοΏ½?οΏ½θ‘Œγ—γŸγγͺγ„ε ΄εˆγ―ηœη•₯します。
    • 省η•₯可能 bootstrapModules: bootstrapScripts γ¨εŒζ§˜γ§γ™γŒγ€δ»£γ‚γ‚Šγ« <script type="module"> γ‚’ε‡ΊεŠ›γ—γΎγ™γ€‚
    • 省η•₯可能 identifierPrefix: React が useId γ«γ‚ˆγ£γ¦η”Ÿζˆγ™γ‚‹ ID γ«δ½Ώη”¨γ™γ‚‹ζ–‡ε­—εˆ—γƒ—γƒ¬γƒ•γ‚£γƒƒγ‚―γ‚Ήγ€‚εŒγ˜γƒšγƒΌγ‚ΈδΈŠγ«θ€‡ζ•°οΏ½?γƒ«γƒΌγƒˆγ‚’δ½Ώη”¨γ™γ‚‹ιš›γ«γ€η«Άεˆγ‚’ιΏγ‘γ‚‹γŸγ‚γ«η”¨γ„γΎγ™γ€‚hydrateRoot γ«γ‚‚εŒγ˜γƒ—γƒ¬γƒ•γ‚£γƒƒγ‚―γ‚Ήγ‚’ζΈ‘γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚
    • 省η•₯可能 namespaceURI: こ�?γ‚ΉγƒˆγƒͺγƒΌγƒ οΏ½?γƒ«γƒΌγƒˆγƒγƒΌγƒ γ‚ΉγƒšγƒΌγ‚Ή URI ζ–‡ε­—εˆ—γ€‚γƒ‡γƒ•γ‚©γƒ«γƒˆγ§γ―ι€šεΈΈοΏ½? HTML です。SVG οΏ½?場合は 'http://www.w3.org/2000/svg'、MathML οΏ½?場合は 'http://www.w3.org/1998/Math/MathML' を渑します。
    • 省η•₯可能 nonce: script-src Content-Security-Policy を用いてスクγƒͺγƒ—γƒˆγ‚’θ¨±ε―γ™γ‚‹γŸγ‚οΏ½? nonce ζ–‡ε­—εˆ—γ€‚
    • 省η•₯可能 onError: γ‚΅γƒΌγƒγ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ™γ‚‹γŸγ³γ«η™Ίη«γ™γ‚‹γ‚³γƒΌγƒ«γƒγƒƒγ‚―γ€‚εΎ©εΈ°ε―θƒ½γͺエラー�?ε ΄εˆγ‚‚γγ†γ§γͺいエラー�?ε ΄εˆγ‚‚γ‚γ‚ŠγΎγ™γ€‚γƒ‡γƒ•γ‚©γƒ«γƒˆγ§γ― console.error οΏ½?γΏγ‚’ε‘Όγ³ε‡Ίγ—γΎγ™γ€‚γ“γ‚Œγ‚’δΈŠζ›Έγγ—γ¦γ‚―γƒ©γƒƒγ‚·γƒ₯γƒ¬γƒγƒΌγƒˆγ‚’γƒ­γ‚°γ«θ¨˜ιŒ²γ™γ‚‹ε ΄εˆγ§γ‚‚ console.error γ‚’ε‘Όγ³ε‡Ίγ™γ‚ˆγ†γ«γ—γ¦γγ γ•γ„γ€‚γΎγŸγ€γ‚·γ‚§γƒ«γŒε‡ΊεŠ›γ•γ‚Œγ‚‹ε‰γ«γ‚Ήγƒ†γƒΌγ‚Ώγ‚Ήγ‚³γƒΌγƒ‰γ‚’θͺΏζ•΄γ™γ‚‹γŸγ‚γ«γ‚‚使用できます。
    • 省η•₯可能 progressiveChunkSize: チャンク�?γƒγ‚€γƒˆζ•°γ€‚γƒ‡γƒ•γ‚©γƒ«γƒˆοΏ½?ζŽ¨θ«–ζ–Ήζ³•γ«γ€γ„γ¦γ―γ“γ‘γ‚‰γ‚’ε‚η…§γ—γ¦γγ γ•γ„γ€‚
    • 省η•₯可能 signal: ァーバで�?γƒ¬γƒ³γƒ€γƒΌγ‚’δΈ­ζ­’γ—γ¦γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ§οΏ½?οΏ½γ‚Šγ‚’γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹γŸγ‚γ«δ½Ώη”¨γ§γγ‚‹ abort signal。

θΏ”γ‚Šε€€

renderToReadableStream は Promise を返します。

θΏ”γ•γ‚Œγ‚‹γ‚Ήγƒˆγƒͺームにはδ»₯δΈ‹οΏ½?追加�?γƒ—γƒ­γƒ‘γƒ†γ‚£γŒε­˜εœ¨γ—γΎγ™γ€‚

  • allReady: シェルとすべて�?θΏ½εŠ γ‚³γƒ³γƒ†γƒ³γƒ„οΏ½?丑方を含むすべて�?γƒ¬γƒ³γƒ€γƒΌγŒοΏ½?οΏ½δΊ†γ—γŸγ¨γγ«θ§£ζ±Ίγ•γ‚Œγ‚‹ Promiseγ€‚γ‚―γƒ­γƒΌγƒ©γ‚„ι™ηš„η”Ÿζˆε‘γ‘οΏ½?ε ΄εˆγ€γƒ¬γ‚Ήγƒγƒ³γ‚Ήγ‚’θΏ”γ™ε‰γ« stream.allReady γ‚’ await γ§γγΎγ™γ€‚γ“γ‚Œγ‚’θ‘Œγ†γ¨γƒ—γƒ­γ‚°γƒ¬γƒƒγ‚·γƒ–γͺγƒ­γƒΌγƒ‡γ‚£γƒ³γ‚°γŒγͺくγͺγ‚Šγ€γ‚ΉγƒˆγƒͺγƒΌγƒ γ«γ―ζœ€η΅‚ηš„γͺ HTML γŒε«γΎγ‚Œγ‚‹γ‚ˆγ†γ«γͺγ‚ŠγΎγ™γ€‚

使用法

React ツγƒͺγƒΌγ‚’ HTML としてθͺ­γΏε–γ‚Šε―能γͺ Web Stream にレンダーする

renderToReadableStream を呼び出して、React ツγƒͺγƒΌγ‚’ HTML としてθͺ­γΏε–γ‚Šε―能γͺ Web Stream にレンダーします。

import { renderToReadableStream } from 'react-dom/server';

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

γƒ«γƒΌγƒˆγ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆ と γƒ–γƒΌγƒˆγ‚Ήγƒˆγƒ©γƒƒγƒ— <script> パス�?γƒͺγ‚Ήγƒˆγ‚’ζŒ‡οΏ½?οΏ½γ™γ‚‹εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚γƒ«γƒΌγƒˆγ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ―γ€γƒ«γƒΌγƒˆοΏ½? <html> タグを含んだドキγƒ₯γƒ‘γƒ³γƒˆε…¨δ½“γ‚’θΏ”γ™γ‚ˆγ†γ«γ—γΎγ™γ€‚

δΎ‹γˆγ°δ»₯δΈ‹οΏ½?γ‚ˆγ†γͺ归にγͺるでしょう。

export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}

React は doctype とあγͺγŸγŒζŒ‡οΏ½?οΏ½γ—γŸγƒ–γƒΌγƒˆγ‚Ήγƒˆγƒ©γƒƒγƒ— <script> γ‚Ώγ‚°γ‚’η΅ζžœοΏ½? HTML γ‚Ήγƒˆγƒͺームに注ε…₯します。

<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>

γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆε΄γ§γ―γ€γƒ–γƒΌγƒˆγ‚Ήγƒˆγƒ©γƒƒγƒ—γ‚Ήγ‚―γƒͺγƒ—γƒˆγ― hydrateRoot を呼び出して document 全体�?γƒγ‚€γƒ‰γƒ¬γƒΌγ‚·γƒ§γƒ³γ‚’θ‘Œγ†εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App />);

γ“γ‚Œγ«γ‚ˆγ‚Šγ€γ‚΅γƒΌγƒγ§η”Ÿζˆγ•γ‚ŒγŸ HTML γ«γ‚€γƒ™γƒ³γƒˆγƒͺγ‚ΉγƒŠγŒθΏ½εŠ γ•γ‚Œγ€ζ“δ½œε―θƒ½γ«γͺγ‚ŠγΎγ™γ€‚

さらに深くηŸ₯γ‚‹

γƒ“γƒ«γƒ‰ε‡ΊεŠ›γ‹γ‚‰ CSS と JS οΏ½?γ‚’γ‚»γƒƒγƒˆγƒ‘γ‚Ήγ‚’θͺ­γΏε–γ‚‹

γƒ“γƒ«γƒ‰εΎŒγ«γ€ζœ€η΅‚ηš„γͺγ‚’γ‚»γƒƒγƒˆ URL(JavaScript γ‚„ CSS フゑむルγͺγ©οΌ‰γ«γ―γ‚ˆγγƒγƒƒγ‚·γƒ₯εŒ–γŒθ‘Œγ‚γ‚ŒγΎγ™γ€‚δΎ‹γˆγ°γ€styles.css が styles.123456.css にγͺγ‚‹γ“γ¨γŒγ‚γ‚ŠγΎγ™γ€‚ι™ηš„γͺγ‚’γ‚»γƒƒγƒˆοΏ½?フゑむル名をハッシγƒ₯εŒ–γ™γ‚‹γ“γ¨γ§γ€εŒγ˜γ‚’γ‚»γƒƒγƒˆγŒγƒ“γƒ«γƒ‰γ”γ¨γ«η•°γͺるフゑむル名にγͺγ‚‹γ“γ¨γŒδΏθ¨Όγ•γ‚ŒγΎγ™γ€‚γ“γ‚ŒγŒζœ‰η”¨γͺοΏ½?は、ある特�?οΏ½οΏ½?εε‰γ‚’ζŒγ€γƒ•γ‚‘γ‚€γƒ«οΏ½?ε†…οΏ½?οΏ½γŒδΈε€‰γ«γͺγ‚Šγ€ι™ηš„γͺγ‚’γ‚»γƒƒγƒˆοΏ½?ι•·ζœŸηš„γͺキャッシングを�?οΏ½ε…¨γ«θ‘Œγˆγ‚‹γ‚ˆγ†γ«γͺγ‚‹γŸγ‚γ§γ™γ€‚

γ—γ‹γ—γ€γƒ“γƒ«γƒ‰εΎŒγΎγ§γ‚’γ‚»γƒƒγƒˆ URL γŒεˆ†γ‹γ‚‰γͺγ„ε ΄εˆγ€γγ‚Œγ‚‰γ‚’γ‚½γƒΌγ‚Ήγ‚³γƒΌγƒ‰γ«ε«γ‚γ‚‹γ“γ¨γŒγ§γγΎγ›γ‚“γ€‚δΎ‹γˆγ°γ€ε…ˆγ»γ©οΏ½?γ‚ˆγ†γ« JSX に "/styles.css" γ‚’γƒγƒΌγƒ‰γ‚³γƒΌγƒ‡γ‚£γƒ³γ‚°γ™γ‚‹ζ–Ήζ³•γ―ε‹•δ½œγ—γΎγ›γ‚“γ€‚γ‚½γƒΌγ‚Ήγ‚³γƒΌγƒ‰γ«γγ‚Œγ‚‰γ‚’ε«γ‚γͺγ„γ‚ˆγ†γ«γ™γ‚‹γŸγ‚γ€γƒ«γƒΌγƒˆγ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγŒγ€props η΅Œη”±γ§ζΈ‘γ•γ‚ŒγŸγƒžγƒƒγƒ—γ‹γ‚‰οΏ½?οΏ½ιš›οΏ½?フゑむル名をθͺ­γΏε–γ‚‹γ‚ˆγ†γ«γ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚

export default function App({ assetMap }) {
return (
<html>
<head>
<title>My app</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}

γ‚΅γƒΌγƒδΈŠγ§γ―γ€<App assetMap={assetMap} /> οΏ½?γ‚ˆγ†γ«γƒ¬γƒ³γƒ€γƒΌγ—γ€γ‚’γ‚»γƒƒγƒˆ URL を含む assetMap を渑します。

// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

ァーバで <App assetMap={assetMap} /> οΏ½?γ‚ˆγ†γ«γƒ¬γƒ³γƒ€γƒΌγ—γ¦γ„γ‚‹οΏ½?γ§γ€γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ§γ‚‚ assetMap γ‚’δ½Ώγ£γ¦γƒ¬γƒ³γƒ€γƒΌγ—γ¦γƒγ‚€γƒ‰γƒ¬γƒΌγ‚·γƒ§γƒ³γ‚¨γƒ©γƒΌγ‚’ιΏγ‘γ‚‹εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚γ“οΏ½?γŸγ‚γ«γ―δ»₯δΈ‹οΏ½?γ‚ˆγ†γ« assetMap γ‚’γ‚·γƒͺγ‚’γƒ©γ‚€γ‚Ίγ—γ¦γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ«ζΈ‘γ—γΎγ™γ€‚

// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

上記�?例では、bootstrapScriptContent γ‚ͺプションを使って<script> γ‚Ώγ‚°γ‚’θΏ½εŠ γ—γ¦γ€γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§γ‚°γƒ­γƒΌγƒγƒ« window.assetMap ε€‰ζ•°γ‚’γ‚»γƒƒγƒˆγ—γ¦γ„γΎγ™γ€‚γ“γ‚Œγ«γ‚ˆγ‚Šγ€γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆοΏ½?γ‚³γƒΌγƒ‰γŒεŒγ˜ assetMap γ‚’θͺ­γΏε–γ‚Œγ‚‹γ‚ˆγ†γ«γͺγ‚ŠγΎγ™γ€‚

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App assetMap={window.assetMap} />);

γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ¨γ‚΅γƒΌγƒοΏ½?δΈ‘ζ–ΉγŒ props γ¨γ—γ¦εŒγ˜ assetMap を使って App γ‚’γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹γŸγ‚γ€γƒγ‚€γƒ‰γƒ¬γƒΌγ‚·γƒ§γƒ³γ‚¨γƒ©γƒΌγ―η™Ίη”Ÿγ—γΎγ›γ‚“γ€‚


γƒ­γƒΌγƒ‰γŒι€²γ‚€γ«γ€γ‚Œγ¦γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ™γ‚‹

γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ«γ‚ˆγ‚Šγ€γ‚΅γƒΌγƒδΈŠγ§γ™γΉγ¦οΏ½?γƒ‡γƒΌγ‚ΏγŒγƒ­γƒΌγƒ‰γ•γ‚Œγ‚‹ε‰γ«γ€γƒ¦γƒΌγ‚ΆγŒγ‚³γƒ³γƒ†γƒ³γƒ„γ‚’θ¦‹ε§‹γ‚γ‚‰γ‚Œγ‚‹γ‚ˆγ†γ«γ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚δΎ‹γˆγ°δ»₯δΈ‹οΏ½?γ‚ˆγ†γͺγƒ—γƒ­γƒ•γ‚£γƒΌγƒ«γƒšγƒΌγ‚ΈγŒγ‚γ‚Šγ€γ‚«γƒγƒΌγ€γƒ•γƒ¬γƒ³γƒ‰γƒ»ε†™ηœŸγŒε«γΎγ‚ŒγŸγ‚΅γ‚€γƒ‰γƒγƒΌγ€ζŠ•η¨ΏοΏ½?γƒͺγ‚Ήγƒˆγ‚’θ‘¨η€Ίγ—γ¦γ„γ‚‹γ¨γ“γ‚γ‚’θ€ƒγˆγΎγ—γ‚‡γ†γ€‚

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}

ここで、<Posts /> οΏ½?データをθͺ­γΏθΎΌγ‚€οΏ½?γ«ζ™‚ι–“γŒγ‹γ‹γ‚‹γ¨γ—γΎγ—γ‚‡γ†γ€‚η†ζƒ³ηš„γ«γ―γ€ζŠ•η¨ΏοΏ½?θͺ­γΏθΎΌγΏγ‚’待぀ことγͺγγ€γƒ—γƒ­γƒ•γ‚£γƒΌγƒ«γƒšγƒΌγ‚ΈοΏ½?οΏ½?οΏ½γ‚ŠοΏ½?γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’γƒ¦γƒΌγ‚Άγ«θ‘¨η€Ίγ—γŸγ„γ§γ—γ‚‡γ†γ€‚γ“γ‚Œγ‚’οΏ½?�現するには、Posts γ‚’ <Suspense> バウンダγƒͺで囲みます。

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

γ“γ‚Œγ«γ‚ˆγ‚Š React に、Posts οΏ½?γƒ‡γƒΌγ‚ΏγŒθͺ­γΏθΎΌγΎγ‚Œγ‚‹ε‰γ« HTML γ‚’γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°ι–‹ε§‹γ™γ‚‹γ‚ˆγ†ζŒ‡η€Ίγ—γΎγ™γ€‚React γ―γΎγšγ€γƒ­γƒΌγƒ‡γ‚£γƒ³γ‚°γƒ•γ‚©γƒΌγƒ«γƒγƒƒγ‚― (PostsGlimmer) οΏ½? HTML を送俑します。欑に Posts οΏ½?データθͺ­γΏθΎΌγΏγŒοΏ½?οΏ½δΊ†γ—γŸγ‚‰γ€οΏ½?οΏ½γ‚ŠοΏ½? HTML γ¨γ€γƒ­γƒΌγƒ‡γ‚£γƒ³γ‚°γƒ•γ‚©γƒΌγƒ«γƒγƒƒγ‚―γ‚’γγ‚Œγ§οΏ½?ζ›γ™γ‚‹γŸγ‚οΏ½?むンラむン <script> γ‚Ώγ‚°γ‚’ι€δΏ‘γ—γΎγ™γ€‚γƒ¦γƒΌγ‚Άγ‹γ‚‰θ¦‹γ‚‹γ¨γ€γƒšγƒΌγ‚Έγ«γ―γΎγš PostsGlimmer γŒθ‘¨η€Ίγ•γ‚Œγ€εΎŒγ‹γ‚‰γγ‚ŒγŒ Posts に�?き換わることにγͺγ‚ŠγΎγ™γ€‚

γ•γ‚‰γ«γ€γ‚ˆγ‚Šη΄°γ‹γθͺ­γΏθΎΌγΏγ‚·γƒΌγ‚±γƒ³γ‚Ήγ‚’εˆΆεΎ‘γ™γ‚‹γŸγ‚γ«<Suspense> バウンダγƒͺγ‚’γƒγ‚Ήγƒˆγ•γ›γ‚‹γ“γ¨γ‚‚γ§γγΎγ™γ€‚

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}

こ�?例では、React γ―γƒšγƒΌγ‚ΈοΏ½?γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ‚’γ•γ‚‰γ«η΄ ζ—©γι–‹ε§‹γ§γγΎγ™γ€‚ζœ€εˆγ«γƒ¬γƒ³γƒ€γƒΌγŒοΏ½?οΏ½δΊ†γ—γ¦γ„γ‚‹εΏ…θ¦γŒγ‚γ‚‹οΏ½?は、<Suspense> バウンダγƒͺγ§ε›²γΎγ‚Œγ¦γ„γͺい ProfileLayout と ProfileCover だけです。Sidebar、Friends、Photos γŒγƒ‡γƒΌγ‚Ώγ‚’θͺ­γΏθΎΌγ‚€εΏ…θ¦γŒγ‚γ‚‹ε ΄εˆγ€React は BigSpinner οΏ½?フォールバック HTML γ‚’δ»£γ‚γ‚Šγ«ι€δΏ‘γ—γΎγ™γ€‚γοΏ½?εΎŒγ€γ‚ˆγ‚Šε€šγοΏ½?γƒ‡γƒΌγ‚ΏγŒεˆ©η”¨ε―θƒ½γ«γͺγ‚‹γ«γ€γ‚Œγ€γ‚ˆγ‚Šε€šγοΏ½?γ‚³γƒ³γƒ†γƒ³γƒ„γŒθ‘¨η€Ίγ•γ‚Œγ¦γ„γγ€ζœ€η΅‚ηš„γ«γ™γΉγ¦γŒθ‘¨η€Ίγ•γ‚ŒγΎγ™γ€‚

γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ§γ―γ€γƒ–γƒ©γ‚¦γ‚Άγ§ React θ‡ͺδ½“γŒθͺ­γΏθΎΌγΎγ‚Œγ‚‹οΏ½?を待぀必要も、をプγƒͺγŒζ“δ½œε―θƒ½γ«γͺγ‚‹οΏ½?γ‚’εΎ…γ€εΏ…θ¦γ‚‚γ‚γ‚ŠγΎγ›γ‚“γ€‚γ‚΅γƒΌγƒγ‹γ‚‰οΏ½? HTML コンテンツは、あらゆる <script> γ‚Ώγ‚°γŒθͺ­γΏθΎΌγΎγ‚Œγ‚‹ε‰γ«γƒ—γƒ­γ‚°γƒ¬γƒƒγ‚·γƒ–γ«θ‘¨η€Ίγ•γ‚ŒγΎγ™γ€‚

HTML γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°οΏ½?ε‹•δ½œγ«γ€γ„γ¦θ©³γ—γθͺ­γ‚€

補袳

γ‚΅γ‚Ήγƒšγƒ³γ‚Ήγ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‚’γ‚’γ‚―γƒ†γ‚£γƒ–εŒ–γ§γγ‚‹οΏ½?γ―γ‚΅γ‚Ήγƒšγƒ³γ‚Ήε―ΎεΏœοΏ½?γƒ‡γƒΌγ‚Ώγ‚½γƒΌγ‚Ήγ γ‘γ§γ™γ€‚γ“γ‚Œγ«γ―δ»₯δΈ‹γŒε«γΎγ‚ŒγΎγ™οΌš

  • Relay γ‚„ Next.js οΏ½?γ‚ˆγ†γͺγ‚΅γ‚Ήγƒšγƒ³γ‚Ήε―ΎεΏœοΏ½?フレームワークで�?データフェッチ
  • lazy γ‚’η”¨γ„γŸγ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‚³γƒΌγƒ‰οΏ½?遅廢ロード
  • use γ‚’η”¨γ„γŸγƒ—γƒ­γƒŸγ‚Ή (Promise) から�?ε€€οΏ½?θͺ­γΏε–γ‚Š

γ‚΅γ‚Ήγƒšγƒ³γ‚Ήγ―γ‚¨γƒ•γ‚§γ‚―γƒˆγ‚„γ‚€γƒ™γƒ³γƒˆγƒγƒ³γƒ‰γƒ©ε†…γ§γƒ‡γƒΌγ‚Ώγƒ•γ‚§γƒƒγƒγŒθ‘Œγ‚γ‚ŒγŸε ΄εˆγ«γ―γγ‚Œγ‚’ζ€œε‡Ίγ—γΎγ›γ‚“γ€‚

上記�? Posts γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ§οΏ½?οΏ½ιš›γ«γƒ‡γƒΌγ‚Ώγ‚’γƒ­γƒΌγƒ‰γ™γ‚‹ζ–Ήζ³•γ―γ€δ½Ώη”¨γ™γ‚‹γƒ•γƒ¬γƒΌγƒ γƒ―γƒΌγ‚―γ«γ‚ˆγ£γ¦η•°γͺγ‚ŠγΎγ™γ€‚γ‚΅γ‚Ήγƒšγƒ³γ‚Ήε―ΎεΏœοΏ½?γƒ•γƒ¬γƒΌγƒ γƒ―γƒΌγ‚―γ‚’δ½Ώη”¨γ—γ¦γ„γ‚‹ε ΄εˆγ€θ©³η΄°γ―γƒ‡γƒΌγ‚Ώγƒ•γ‚§γƒƒγƒγ«ι–’γ™γ‚‹γƒ‰γ‚­γƒ₯γƒ‘γƒ³γƒ†γƒΌγ‚·γƒ§γƒ³ε†…γ«θ¨˜θΌ‰γ•γ‚Œγ¦γ„γ‚‹γ―γšγ§γ™γ€‚

使い方�?規約�?ある (opinionated) γƒ•γƒ¬γƒΌγƒ γƒ―γƒΌγ‚―γ‚’δ½Ώη”¨γ›γšγ«γ‚΅γ‚Ήγƒšγƒ³γ‚Ήγ‚’δ½Ώγ£γŸγƒ‡γƒΌγ‚Ώγƒ•γ‚§γƒƒγƒγ‚’θ‘Œγ†γ“γ¨γ―γ€γΎγ γ‚΅γƒγƒΌγƒˆγ•γ‚Œγ¦γ„γΎγ›γ‚“γ€‚γ‚΅γ‚Ήγƒšγƒ³γ‚Ήε―ΎεΏœοΏ½?データソースを�?οΏ½θ£…γ™γ‚‹γŸγ‚οΏ½?要仢はまだ不�?οΏ½οΏ½?οΏ½γ§γ‚γ‚Šγ€γƒ‰γ‚­γƒ₯γƒ‘γƒ³γƒˆεŒ–γ•γ‚Œγ¦γ„γΎγ›γ‚“γ€‚γƒ‡γƒΌγ‚Ώγ‚½γƒΌγ‚Ήγ‚’γ‚΅γ‚Ήγƒšγƒ³γ‚Ήγ¨η΅±εˆγ™γ‚‹γŸγ‚οΏ½?公式γͺ API は、React οΏ½?ε°†ζ₯οΏ½?バージョンでγƒͺγƒͺγƒΌγ‚Ήγ•γ‚Œγ‚‹δΊˆοΏ½?�です。


シェルに何を含めるか�?ζŒ‡οΏ½?οΏ½

γ‚’γƒ—γƒͺοΏ½?ε…¨ <Suspense> バウンダγƒͺγ‚ˆγ‚Šε€–γ«γ‚γ‚‹ιƒ¨εˆ†οΏ½?ことをシェル (shell) と呼びます。

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}

γ“γ‚ŒγŒγ€γƒ¦γƒΌγ‚Άγ«θ¦‹γˆγ‚‹ζœ€εˆοΏ½?γƒ­γƒΌγƒ‡γ‚£γƒ³γ‚°δΈ­ηŠΆζ…‹γ‚’ζ±ΊοΏ½?�します。

<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>

γ‚‚γ—γƒ«γƒΌγƒˆιƒ¨εˆ†γ§γ‚’γƒ—γƒͺ全体を <Suspense> バウンダγƒͺでラップしてしまうと、シェルとしてはそ�?γ‚Ήγƒ”γƒŠγ γ‘γŒε«γΎγ‚Œγ‚‹γ“γ¨γ«γͺγ‚ŠγΎγ™γ€‚γ—γ‹γ—γ“γ‚Œγ―γ‚γΎγ‚ŠεΏ«ι©γͺユーア体験にはγͺγ‚ŠγΎγ›γ‚“γ€‚ε€§γγͺγ‚Ήγƒ”γƒŠγŒη”»ι’γ«θ‘¨η€Ίγ•γ‚Œγ‚‹γ“γ¨γ―γ€γ‚‚γ†ε°‘γ—γ γ‘εΎ…γ£γ¦γ‹γ‚‰οΏ½?οΏ½ιš›οΏ½?γƒ¬γ‚€γ‚’γ‚¦γƒˆγ‚’θ‘¨η€Ίγ™γ‚‹γ“γ¨γ‚ˆγ‚Šγ‚‚ι…γδΈεΏ«γ«ζ„Ÿγ˜γ‚‰γ‚Œγ‚‹γŸγ‚γ§γ™γ€‚γ—γŸγŒγ£γ¦γ€<Suspense> ε’ƒη•Œγ―ι©εˆ‡γ«ι…οΏ½?γ—γ¦γ€γ‚·γ‚§γƒ«γŒγƒŸγƒ‹γƒžγƒ«γ‹γ€οΏ½?οΏ½ε…¨γ«ζ„Ÿγ˜γ‚‰γ‚Œγ‚‹γ‚ˆγ†γ«εΏ…θ¦γŒγ‚γ‚‹γ§γ—γ‚‡γ†γ€‚δΎ‹γˆγ°γƒšγƒΌγ‚Έγƒ¬γ‚€γ‚’γ‚¦γƒˆε…¨δ½“οΏ½?γ‚Ήγ‚±γƒ«γƒˆγƒ³οΏ½?γ‚ˆγ†γͺγ‚‚οΏ½?です。

非同期�? renderToReadableStream ε‘Όγ³ε‡Ίγ—γŒ stream γ«θ§£ζ±Ίγ•γ‚Œγ‚‹οΏ½?は、シェル全体�?γƒ¬γƒ³γƒ€γƒΌγŒη΅‚δΊ†γ—γŸη›΄εΎŒγ§γ™γ€‚ι€šεΈΈγ€γοΏ½? stream γ‚’δ½Ώγ£γ¦γƒ¬γ‚Ήγƒγƒ³γ‚Ήγ‚’δ½œζˆγ—γ¦θΏ”γ™γ“γ¨γ§γ€γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ‚’ι–‹ε§‹γ—γΎγ™γ€‚

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

こ�? stream γŒθΏ”γ•γ‚Œγ‚‹ζ™‚η‚Ήγ§γ―γ€γƒγ‚Ήγƒˆγ•γ‚ŒγŸ <Suspense> バウンダγƒͺε†…οΏ½?γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ―γΎγ γƒ‡γƒΌγ‚Ώγ‚’γƒ­γƒΌγƒ‰δΈ­γ‹γ‚‚γ—γ‚ŒγΎγ›γ‚“γ€‚


γ‚΅γƒΌγƒδΈŠγ§οΏ½?クラッシγƒ₯γƒ­γ‚°οΏ½?記録

γƒ‡γƒ•γ‚©γƒ«γƒˆγ§γ―γ€γ‚΅γƒΌγƒδΈŠοΏ½?すべて�?γ‚¨γƒ©γƒΌγ―γ‚³γƒ³γ‚½γƒΌγƒ«γ«γƒ­γ‚°γ¨γ—γ¦θ¨˜ιŒ²γ•γ‚ŒγΎγ™γ€‚γ“οΏ½?ζŒ™ε‹•γ‚’γ‚ͺーバーラむドして、クラッシγƒ₯γƒ¬γƒγƒΌγƒˆγ‚’γƒ­γ‚°γ¨γ—γ¦θ¨˜ιŒ²γ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚

async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}

γ‚«γ‚Ήγ‚Ώγƒ οΏ½? onError οΏ½?οΏ½θ£…γ‚’ζδΎ›γ™γ‚‹ε ΄εˆγ€δΈŠθ¨˜οΏ½?γ‚ˆγ†γ«γ‚¨γƒ©γƒΌγ‚’γ‚³γƒ³γ‚½γƒΌγƒ«γ«γ‚‚γƒ­γ‚°γ¨γ—γ¦θ¨˜ιŒ²γ™γ‚‹γ“γ¨γ‚’εΏ˜γ‚Œγͺいでください。


シェル内�?エラーから�?εΎ©εΈ°

こ�?例では、シェルとして ProfileLayout、ProfileCoverγ€γŠγ‚ˆγ³ PostsGlimmer γŒε«γΎγ‚Œγ¦γ„γΎγ™γ€‚

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

γ“γ‚Œγ‚‰οΏ½?γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‚’γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹ιš›γ«γ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ—γŸε ΄εˆγ€React γ―γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ«ι€δΏ‘γ§γγ‚‹ζ„ε‘³οΏ½?ある HTML γ‚’ζδΎ›γ§γγΎγ›γ‚“γ€‚ζœ€η΅‚ζ‰‹οΏ½?�として、renderToReadableStream 呼び出しを try...catch でラップして、ァーバレンダγƒͺγƒ³γ‚°γ«δΎε­˜γ—γͺいフォールバック HTML を送俑しましょう。

async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}

シェル�?η”ŸζˆδΈ­γ«γ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ—γŸε ΄εˆγ€onError と catch ブロック�?δΈ‘ζ–ΉγŒη™Ίη«γ—γΎγ™γ€‚γ‚¨γƒ©γƒΌγƒ¬γƒγƒΌγƒˆγ«γ― onError を使用し、フォールバック�? HTML ドキγƒ₯γƒ‘γƒ³γƒˆγ‚’ι€δΏ‘γ™γ‚‹γŸγ‚γ«γ― catch ブロックを使用します。フォールバック HTML γ―γ‚¨γƒ©γƒΌγƒšγƒΌγ‚Έγ§γ‚γ‚‹εΏ…θ¦γ―γ‚γ‚ŠγΎγ›γ‚“γ€‚δ»£γ‚γ‚Šγ«γ€γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆοΏ½?みでをプγƒͺγ‚’γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹γŸγ‚οΏ½?代替シェルを含めることも可能です。


シェル倖�?エラーから�?εΎ©εΈ°

こ�?例では、<Posts /> γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ― <Suspense> γ§γƒ©γƒƒγƒ—γ•γ‚Œγ¦γ„γ‚‹γŸγ‚γ€γ‚·γ‚§γƒ«οΏ½?δΈ€ιƒ¨γ§γ―γ‚γ‚ŠγΎγ›γ‚“γ€‚

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

Posts γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγΎγŸγ―γοΏ½?内部�?γ©γ“γ‹γ§γ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ—γŸε ΄εˆγ€React はそこから�?復帰を試みます。

  1. ζœ€γ‚‚θΏ‘γ„ <Suspense> バウンダγƒͺ (PostsGlimmer) οΏ½?ローディングフォールバックを HTML γ¨γ—γ¦ε‡ΊεŠ›γ—γΎγ™γ€‚
  2. γ‚΅γƒΌγƒδΈŠγ§ Posts οΏ½?γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’γƒ¬γƒ³γƒ€γƒΌγ—γ‚ˆγ†γ¨γ™γ‚‹οΏ½?を諦めます。
  3. JavaScript γ‚³γƒΌγƒ‰γŒγ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§γƒ­γƒΌγƒ‰γ•γ‚Œγ‚‹γ¨γ€React γ―γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§ Posts οΏ½?γƒ¬γƒ³γƒ€γƒΌγ‚’ε†θ©¦θ‘Œγ—γΎγ™γ€‚

γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§ Posts οΏ½?γƒ¬γƒ³γƒ€γƒΌγ‚’ε†θ©¦θ‘Œγ—γ¦ε†εΊ¦ε€±ζ•—γ—γŸε ΄εˆγ€React γ―γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§γ‚¨γƒ©γƒΌγ‚’γ‚Ήγƒ­γƒΌγ—γΎγ™γ€‚γƒ¬γƒ³γƒ€γƒΌδΈ­γ«γ‚Ήγƒ­γƒΌγ•γ‚Œγ‚‹δ»–οΏ½?すべて�?γ‚¨γƒ©γƒΌγ¨εŒζ§˜γ«γ€ζœ€γ‚‚θΏ‘γ„θ¦ͺοΏ½?エラーバウンダγƒͺγŒγƒ¦γƒΌγ‚Άγ«γ‚¨γƒ©γƒΌγ‚’γ©οΏ½?γ‚ˆγ†γ«ζη€Ίγ™γ‚‹γ‹γ‚’ζ±ΊοΏ½?οΏ½γ—γΎγ™γ€‚γ€γΎγ‚Šγ€γ‚¨γƒ©γƒΌγŒεΎ©εΈ°δΈθƒ½γ§γ‚γ‚‹γ“γ¨γŒη’ΊοΏ½?οΏ½γ™γ‚‹γΎγ§γ€γƒ¦γƒΌγ‚Άγ«γ―γƒ­γƒΌγƒ‡γ‚£γƒ³γ‚°γ‚€γƒ³γ‚Έγ‚±γƒΌγ‚ΏγŒθ¦‹γˆγ‚‹γ“γ¨γ«γͺγ‚ŠγΎγ™γ€‚

γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§οΏ½? Posts οΏ½?γƒ¬γƒ³γƒ€γƒΌε†θ©¦θ‘ŒγŒζˆεŠŸγ—γŸε ΄εˆγ€γ‚΅γƒΌγƒγ‹γ‚‰οΏ½?γƒ­γƒΌγƒ‡γ‚£γƒ³γ‚°γƒ•γ‚©γƒΌγƒ«γƒγƒƒγ‚―γ―γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ§οΏ½?γƒ¬γƒ³γƒ€γƒΌε‡ΊεŠ›γ§οΏ½?γζ›γˆγ‚‰γ‚ŒγΎγ™γ€‚γƒ¦γƒΌγ‚Άγ«γ―γ‚΅γƒΌγƒγ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ—γŸγ“γ¨γ―εˆ†γ‹γ‚ŠγΎγ›γ‚“γ€‚γŸγ γ—γ€γ‚΅γƒΌγƒοΏ½? onError γ‚³γƒΌγƒ«γƒγƒƒγ‚―γ¨γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆοΏ½? onRecoverableError γ‚³γƒΌγƒ«γƒγƒƒγ‚―γŒη™Ίη«γ™γ‚‹γŸγ‚γ€γ‚¨γƒ©γƒΌγ«γ€γ„γ¦ι€šηŸ₯γ‚’ε—γ‘ε–γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚


ステータスコード�?θ¨­οΏ½?οΏ½

γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ«γ―γƒˆγƒ¬γƒΌγƒ‰γ‚ͺγƒ•γ‚‚ε­˜εœ¨γ—γΎγ™γ€‚γƒ¦γƒΌγ‚ΆγŒγ‚³γƒ³γƒ†γƒ³γƒ„γ‚’ζ—©γθ¦‹γ‚‹γ“γ¨γŒγ§γγ‚‹γ‚ˆγ†γ«γ€γ§γγ‚‹γ γ‘ζ—©γγƒšγƒΌγ‚ΈοΏ½?γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ‚’ι–‹ε§‹γ—γŸγ„γ§γ—γ‚‡γ†γ€‚δΈ€ζ–Ήγ§γ€γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ‚’ι–‹ε§‹γ™γ‚‹γ¨γ€γƒ¬γ‚Ήγƒγƒ³γ‚ΉοΏ½?ステータスコードを設�?οΏ½γ™γ‚‹γ“γ¨γŒγ§γγͺくγͺγ‚ŠγΎγ™γ€‚

γ‚·γ‚§γƒ«οΌˆγ™γΉγ¦οΏ½? <Suspense> バウンダγƒͺγ‚ˆγ‚ŠδΈŠοΏ½?ιƒ¨εˆ†οΌ‰γ¨γγ‚Œδ»₯ε€–οΏ½?コンテンツにをプγƒͺγ‚’εˆ†ε‰²γ™γ‚‹γ“γ¨γ§γ€γ“οΏ½?ε•ι‘Œγ―γ™γ§γ«ιƒ¨εˆ†ηš„γ«θ§£ζ±Ίγ•γ‚Œγ¦γ„γΎγ™γ€‚γ‚·γ‚§γƒ«γ§γ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ—γŸε ΄εˆγ€catch γƒ–γƒ­γƒƒγ‚―γŒοΏ½?οΏ½θ‘Œγ•γ‚Œγ€γ‚¨γƒ©γƒΌοΏ½?γ‚Ήγƒ†γƒΌγ‚Ώγ‚Ήγ‚³γƒΌγƒ‰γ‚’γ‚»γƒƒγƒˆγ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚γγ‚Œδ»₯ε€–οΏ½?ε ΄εˆγ―γ€γ‚’γƒ—γƒͺγŒγ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§εΎ©εΈ°γ§γγ‚‹ε―θƒ½ζ€§γŒγ‚γ‚‹γŸγ‚γ€β€œOK” を送俑できる�?です。

async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}

シェル�?ε€–ε΄οΌˆγ€γΎγ‚Š <Suspense> バウンダγƒͺοΏ½?内側)�?γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ§γ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ—γŸε ΄εˆγ€React γ―γƒ¬γƒ³γƒ€γƒΌγ‚’εœζ­’γ—γΎγ›γ‚“γ€‚γ“γ‚Œγ―γ€onError γ‚³γƒΌγƒ«γƒγƒƒγ‚―γŒη™Ίη«γ™γ‚‹γ‚‚οΏ½?οΏ½?、コードは catch ブロックにε…₯γ‚‰γšγ«οΏ½?οΏ½θ‘Œγ‚’ηΆšγ‘γ‚‹γ“γ¨γ‚’ζ„ε‘³γ—γΎγ™γ€‚γ“γ‚Œγ―δΈŠθ¨˜γ§θͺ¬ζ˜Žγ—γŸγ‚ˆγ†γ«γ€React がそ�?γ‚¨γƒ©γƒΌγ‚’γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§εΎ©εΈ°γ—γ‚ˆγ†γ¨γ™γ‚‹γ‹γ‚‰γ§γ™γ€‚

γŸγ γ—γŠζœ›γΏγ§γ‚γ‚Œγ°γ€δ½•γ‚‰γ‹οΏ½?γ‚¨γƒ©γƒΌγŒθ΅·γγŸγ¨γ„γ†δΊ‹οΏ½?�に基γ₯γ„γŸγ‚Ήγƒ†γƒΌγ‚Ώγ‚Ήγ‚³γƒΌγƒ‰γ‚’θ¨­οΏ½?�することもできます。

async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}

γ“γ‚Œγ―γ€εˆζœŸοΏ½?シェルコンテンツ�?η”ŸζˆδΈ­γ«ζ—’γ«γ‚·γ‚§γƒ«οΏ½?ε€–ε΄γ§η™Ίη”Ÿγ—γŸγ‚¨γƒ©γƒΌγŒζ•ζ‰γ§γγ‚‹γ γ‘γͺοΏ½?で、�?οΏ½ε…¨γ§γ―γ‚γ‚ŠγΎγ›γ‚“γ€‚γ‚γ‚‹γ‚³γƒ³γƒ†γƒ³γƒ„γ§γ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ—γŸγ‹γ©γ†γ‹γ‚’ηŸ₯γ‚‹γ“γ¨γŒι‡θ¦γ§γ‚γ‚Œγ°γ€γγ‚Œγ‚’γ‚·γ‚§γƒ«γ«η§»ε‹•γ•γ›γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚


エラー�?οΏ½?ι‘žγ«γ‚ˆγ£γ¦ε‡¦η†γ‚’εˆ†γ‘γ‚‹

γ‚«γ‚Ήγ‚Ώγƒ οΏ½? Error γ‚΅γƒ–γ‚―γƒ©γ‚Ήγ‚’δ½œζˆγ—γ€instanceof ζΌ”οΏ½?�子を使用してどんγͺγ‚¨γƒ©γƒΌγŒγ‚Ήγƒ­γƒΌγ•γ‚ŒγŸγ‹γ‚’γƒγ‚§γƒƒγ‚―γ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚δΎ‹γˆγ°γ€γ‚«γ‚Ήγ‚Ώγƒ οΏ½? NotFoundError γ‚’οΏ½?οΏ½ηΎ©γ—γ€γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‹γ‚‰γγ‚Œγ‚’γ‚Ήγƒ­γƒΌγ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚γοΏ½?εΎŒγ€onError γ§γ‚¨γƒ©γƒΌγ‚’δΏε­˜γ—γ¦γŠγγ€γ‚¨γƒ©γƒΌοΏ½?οΏ½?ι‘žγ«εΏœγ˜γ¦γƒ¬γ‚Ήγƒγƒ³γ‚Ήγ‚’θΏ”γ™ε‰γ«δ½•γ‹η•°γͺγ‚‹ε‡¦η†γ‚’θ‘Œγ†γ“γ¨γŒγ§γγΎγ™γ€‚

async function handler(request) {
let didError = false;
let caughtError = null;

function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}

try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
}
}

γ—γ‹γ—γ‚·γ‚§γƒ«γ‚’ε‡ΊεŠ›γ—γ¦γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ‚’ι–‹ε§‹γ—γ¦γ—γΎγ†γ¨γ€γ‚Ήγƒ†γƒΌγ‚Ώγ‚Ήγ‚³γƒΌγƒ‰γ‚’ε€‰ζ›΄γ§γγͺくγͺγ‚ŠγΎγ™οΏ½?で注意してください。


γ‚―γƒ­γƒΌγƒ©γ‚„ι™ηš„η”Ÿζˆε‘γ‘γ«ε…¨γ‚³γƒ³γƒ†γƒ³γƒ„οΏ½?θͺ­γΏθΎΌγΏγ‚’εΎ…ζ©Ÿγ™γ‚‹

γ‚ΉγƒˆγƒͺγƒΌγƒŸγƒ³γ‚°γ«γ‚ˆγ‚Šγ€εˆ©η”¨ε―θƒ½γ«γͺγ£γŸι †γ§γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’γƒ¦γƒΌγ‚ΆγŒθ¦‹γˆγ‚‹γ‚ˆγ†γ«γͺγ‚‹γŸγ‚γ€γƒ¦γƒΌγ‚Άδ½“ι¨“γŒε‘δΈŠγ—γΎγ™γ€‚

γ—γ‹γ—γ€γ‚―γƒ­γƒΌγƒ©γŒγƒšγƒΌγ‚Έγ‚’θ¨ͺγ‚ŒγŸε ΄εˆγ‚„γ€γƒ“γƒ«γƒ‰ζ™‚γ«γƒšγƒΌγ‚Έγ‚’η”Ÿζˆγ—γ¦γ„γ‚‹ε ΄εˆγ«γ―γ€γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’εΎγ€…γ«θ‘¨η€Ίγ™γ‚‹οΏ½?ではγͺく、すべて�?γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’ζœ€εˆγ«γƒ­γƒΌγƒ‰γ—γ¦γ‹γ‚‰ζœ€η΅‚ηš„γͺ HTML ε‡ΊεŠ›γ‚’η”Ÿζˆγ—γŸγ„γ§γ—γ‚‡γ†γ€‚

Promise である stream.allReady γ‚’ await することで、すべて�?γ‚³γƒ³γƒ†γƒ³γƒ„γŒθͺ­γΏθΎΌγΎγ‚Œγ‚‹γΎγ§εΎ…ζ©Ÿγ‚’θ‘Œγ†γ“γ¨γŒγ§γγΎγ™γ€‚

async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
let isCrawler = // ... depends on your bot detection strategy ...
if (isCrawler) {
await stream.allReady;
}
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}

ι€šεΈΈοΏ½?γƒ¦γƒΌγ‚Άγ―γ€γ‚Ήγƒˆγƒͺームでθͺ­γΏθΎΌγΎγ‚Œγ‚‹γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’οΏ½?οΏ½ιšŽηš„γ«ε—γ‘ε–γ‚ŠγΎγ™γ€‚γ‚―γƒ­γƒΌγƒ©γ―γ€ε…¨γƒ‡γƒΌγ‚ΏγŒθͺ­γΏθΎΌγΎγ‚ŒγŸεΎŒοΏ½?ζœ€η΅‚ηš„γͺ HTML ε‡ΊεŠ›γ‚’ε—γ‘ε–γ‚ŠγΎγ™γ€‚γ—γ‹γ—γ€γ“γ‚Œγ―γ‚―γƒ­γƒΌγƒ©γŒγ™γΉγ¦οΏ½?γƒ‡γƒΌγ‚Ώγ‚’εΎ…γ€εΏ…θ¦γŒγ‚γ‚‹γ“γ¨γ‚’ζ„ε‘³γ—γ€γοΏ½?中にはθͺ­γΏθΎΌγΏγŒι…いも�?γ‚„γ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ™γ‚‹γ‚‚οΏ½?γ‚‚ε«γΎγ‚Œγ‚‹γ‹γ‚‚γ—γ‚ŒγΎγ›γ‚“γ€‚γ‚’γƒ—γƒͺγ‚±γƒΌγ‚·γƒ§γƒ³γ«γ‚ˆγ£γ¦γ―γ€γ‚―γƒ­γƒΌγƒ©γ«γ‚‚γ‚·γ‚§γƒ«γ‚’ι€δΏ‘γ™γ‚‹γ“γ¨γ‚’ιΈζŠžγ—γ¦γ‚‚ζ§‹γ„γΎγ›γ‚“γ€‚


ァーバレンダγƒͺング�?δΈ­ζ­’

γ‚Ώγ‚€γƒ γ‚’γ‚¦γƒˆεΎŒγ«γ‚΅γƒΌγƒγƒ¬γƒ³γƒ€γƒͺγƒ³γ‚°γ‚’γ€Œθ«¦γ‚γ‚‹γ€γ‚ˆγ†γ«εΌ·εˆΆγ™γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚

async function handler(request) {
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 10000);

const stream = await renderToReadableStream(<App />, {
signal: controller.signal,
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
// ...

React は、�?οΏ½γ‚ŠοΏ½?ローディング中フォールバックを HTML γ¨γ—γ¦η›΄γ‘γ«ε‡ΊεŠ›γ—γ€γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§οΏ½?οΏ½γ‚Šγ‚’γƒ¬γƒ³γƒ€γƒΌγ—γ‚ˆγ†γ¨θ©¦γΏγΎγ™γ€‚