import { renderToReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState, } from '@vitejs/plugin-rsc/rsc' import type { ReactFormState } from 'react-dom/client' import { Root } from '../root.tsx' import { parseRenderRequest } from './request.tsx' // The schema of payload which is serialized into RSC stream on rsc environment // and deserialized on ssr/client environments. export type RscPayload = { // this demo renders/serializes/deserizlies entire root html element // but this mechanism can be changed to render/fetch different parts of components // based on your own route conventions. root: React.ReactNode // server action return value of non-progressive enhancement case returnValue?: { ok: boolean; data: unknown } // server action form state (e.g. useActionState) of progressive enhancement case formState?: ReactFormState } // the plugin by default assumes `rsc` entry having default export of request handler. // however, how server entries are executed can be customized by registering own server handler. export default { fetch: handler } async function handler(request: Request): Promise { // differentiate RSC, SSR, action, etc. const renderRequest = parseRenderRequest(request) request = renderRequest.request // handle server function request let returnValue: RscPayload['returnValue'] | undefined let formState: ReactFormState | undefined let temporaryReferences: unknown | undefined let actionStatus: number | undefined if (renderRequest.isAction === true) { if (renderRequest.actionId) { // action is called via `ReactClient.setServerCallback`. const contentType = request.headers.get('content-type') const body = contentType?.startsWith('multipart/form-data') ? await request.formData() : await request.text() temporaryReferences = createTemporaryReferenceSet() const args = await decodeReply(body, { temporaryReferences }) const action = await loadServerAction(renderRequest.actionId) try { const data = await action.apply(null, args) returnValue = { ok: true, data } } catch (e) { returnValue = { ok: false, data: e } actionStatus = 500 } } else { // otherwise server function is called via `
` // before hydration (e.g. when javascript is disabled). // aka progressive enhancement. const formData = await request.formData() const decodedAction = await decodeAction(formData) try { const result = await decodedAction() formState = await decodeFormState(result, formData) } catch (e) { // there's no single general obvious way to surface this error, // so explicitly return classic 500 response. return new Response('Internal Server Error: server action failed', { status: 500, }) } } } // serialization from React VDOM tree to RSC stream. // we render RSC stream after handling server function request // so that new render reflects updated state from server function call // to achieve single round trip to mutate and fetch from server. const rscPayload: RscPayload = { root: , formState, returnValue, } const rscOptions = { temporaryReferences } const rscStream = renderToReadableStream(rscPayload, rscOptions) // Respond RSC stream without HTML rendering as decided by `RenderRequest` if (renderRequest.isRsc) { return new Response(rscStream, { status: actionStatus, headers: { 'content-type': 'text/x-component;charset=utf-8', }, }) } // Delegate to SSR environment for html rendering. // The plugin provides `loadModule` helper to allow loading SSR environment entry module // in RSC environment. however this can be customized by implementing own runtime communication // e.g. `@cloudflare/vite-plugin`'s service binding. const ssrEntryModule = await import.meta.viteRsc.loadModule< typeof import('./entry.ssr.tsx') >('ssr', 'index') const ssrResult = await ssrEntryModule.renderHTML(rscStream, { formState, // allow quick simulation of javascript disabled browser debugNojs: renderRequest.url.searchParams.has('__nojs'), }) // respond html return new Response(ssrResult.stream, { status: ssrResult.status, headers: { 'Content-type': 'text/html', }, }) } if (import.meta.hot) { import.meta.hot.accept() }