今日から始めるhydratableなReact生活
code:sh
pnpm add react react-dom @remix-run/node @remix-run/react isbot
型ライブラリはお好みでインストールしてください
code:entry.server.tsx
import { PassThrough } from 'node:stream'
import type { AppLoadContext, DataFunctionArgs, EntryContext } from '@remix-run/node'
import { createReadableStreamFromReadable, json } from '@remix-run/node'
import { RemixServer } from '@remix-run/react'
import { isbot } from 'isbot'
import { renderToPipeableStream } from 'react-dom/server'
type ENV = {
ENV: string
BUILD_VERSION: string
}
declare global {
var ENV: ENV
interface Window {
ENV: ENV
}
}
export function loader() {
return json({
ENV: {
ENV: process.env.ENV,
BUILD_VERSION: process.env.BUILD_VERSION,
},
})
}
const ABORT_DELAY = 5_000
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
loadContext: AppLoadContext,
) {
return isbot(request.headers.get('user-agent'))
? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext)
: handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext)
}
function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false
const { pipe, abort } = renderToPipeableStream(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
onAllReady() {
shellRendered = true
const body = new PassThrough()
const stream = createReadableStreamFromReadable(body)
responseHeaders.set('Content-Type', 'text/html')
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
)
pipe(body)
},
onShellError(error: unknown) {
reject(error)
},
onError(error: unknown) {
responseStatusCode = 500
if (shellRendered) {
console.error(error)
}
},
},
)
setTimeout(abort, ABORT_DELAY)
})
}
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false
const { pipe, abort } = renderToPipeableStream(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
async onShellReady() {
shellRendered = true
const body = new PassThrough()
const stream = createReadableStreamFromReadable(body)
responseHeaders.set('Content-Type', 'text/html')
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
)
pipe(body)
},
onShellError(error: unknown) {
reject(error)
},
onError(error: unknown) {
responseStatusCode = 500
if (shellRendered) {
console.error(error)
}
},
},
)
setTimeout(abort, ABORT_DELAY)
})
}
export function handleError(error: unknown, { request }: DataFunctionArgs): void {
console.error(error)
}
code:entry.client.tsx
import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});