Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions packages/plugin-rsc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export default defineConfig({
- [`entry.rsc.tsx`](./examples/starter/src/framework/entry.rsc.tsx)

```tsx
import * as ReactServer from '@vitejs/plugin-rsc/rsc' // re-export of react-server-dom/server.edge and client.edge
import { renderToReadableStream } from '@vitejs/plugin-rsc/rsc'

// the plugin assumes `rsc` entry having default export of request handler
export default async function handler(request: Request): Promise<Response> {
Expand All @@ -143,7 +143,7 @@ export default async function handler(request: Request): Promise<Response> {
</body>
</html>
)
const rscStream = ReactServer.renderToReadableStream(root)
const rscStream = renderToReadableStream(root)

// respond direct RSC stream request based on framework's convention
if (request.url.endsWith('.rsc')) {
Expand Down Expand Up @@ -173,19 +173,19 @@ export default async function handler(request: Request): Promise<Response> {
- [`entry.ssr.tsx`](./examples/starter/src/framework/entry.ssr.tsx)

```tsx
import * as ReactClient from '@vitejs/plugin-rsc/ssr' // re-export of react-server-dom/client.edge
import * as ReactDOMServer from 'react-dom/server.edge'
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
import { renderToReadableStream } from 'react-dom/server.edge'

export async function handleSsr(rscStream: ReadableStream) {
// deserialize RSC stream back to React VDOM
const root = await ReactClient.createFromReadableStream(rscStream)
const root = await createFromReadableStream(rscStream)

// helper API to allow referencing browser entry content from SSR environment
const bootstrapScriptContent =
await import.meta.viteRsc.loadBootstrapScriptContent('index')

// render html (traditional SSR)
const htmlStream = ReactDOMServer.renderToReadableStream(root, {
const htmlStream = renderToReadableStream(root, {
bootstrapScriptContent,
})

Expand All @@ -196,16 +196,16 @@ export async function handleSsr(rscStream: ReadableStream) {
- [`entry.browser.tsx`](./examples/starter/src/framework/entry.browser.tsx)

```tsx
import * as ReactClient from '@vitejs/plugin-rsc/browser' // re-export of react-server-dom/client.browser
import * as ReactDOMClient from 'react-dom/client'
import { createFromReadableStream } from '@vitejs/plugin-rsc/browser'
import { hydrateRoot } from 'react-dom/client'

async function main() {
// fetch and deserialize RSC stream back to React VDOM
const rscResponse = await fetch(window.location.href + '.rsc')
const root = await ReactClient.createFromReadableStream(rscResponse.body)
const root = await createFromReadableStream(rscResponse.body)

// hydration (traditional CSR)
ReactDOMClient.hydrateRoot(document, root)
hydrateRoot(document, root)
}

main()
Expand Down Expand Up @@ -342,13 +342,11 @@ const htmlStream = await renderToReadableStream(reactNode, {
This event is fired when server modules are updated, which can be used to trigger re-fetching and re-rendering of RSC components on browser.

```js
import * as ReactClient from '@vitejs/plugin-rsc/browser'
import { createFromFetch } from '@vitejs/plugin-rsc/browser'

import.meta.hot.on('rsc:update', async () => {
// re-fetch RSC stream
const rscPayload = await ReactClient.createFromFetch(
fetch(window.location.href + '.rsc'),
)
const rscPayload = await createFromFetch(fetch(window.location.href + '.rsc'))
// re-render ...
})
```
Expand Down
24 changes: 15 additions & 9 deletions packages/plugin-rsc/examples/basic/src/framework/entry.browser.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as ReactClient from '@vitejs/plugin-rsc/browser'
import {
createFromReadableStream,
createFromFetch,
setServerCallback,
createTemporaryReferenceSet,
encodeReply,
} from '@vitejs/plugin-rsc/browser'
import React from 'react'
import * as ReactDOMClient from 'react-dom/client'
import { hydrateRoot } from 'react-dom/client'
import { rscStream } from 'rsc-html-stream/client'
import type { RscPayload } from './entry.rsc'

Expand All @@ -10,7 +16,7 @@ async function main() {
let setPayload: (v: RscPayload) => void

// deserialize RSC stream back to React VDOM for CSR
const initialPayload = await ReactClient.createFromReadableStream<RscPayload>(
const initialPayload = await createFromReadableStream<RscPayload>(
// initial RSC stream is injected in SSR stream as <script>...FLIGHT_DATA...</script>
rscStream,
)
Expand All @@ -33,21 +39,21 @@ async function main() {

// re-fetch RSC and trigger re-rendering
async function fetchRscPayload() {
const payload = await ReactClient.createFromFetch<RscPayload>(
const payload = await createFromFetch<RscPayload>(
fetch(window.location.href),
)
setPayload(payload)
}

// register a handler which will be internally called by React
// on server function request after hydration.
ReactClient.setServerCallback(async (id, args) => {
setServerCallback(async (id, args) => {
const url = new URL(window.location.href)
const temporaryReferences = ReactClient.createTemporaryReferenceSet()
const payload = await ReactClient.createFromFetch<RscPayload>(
const temporaryReferences = createTemporaryReferenceSet()
const payload = await createFromFetch<RscPayload>(
fetch(url, {
method: 'POST',
body: await ReactClient.encodeReply(args, { temporaryReferences }),
body: await encodeReply(args, { temporaryReferences }),
headers: {
'x-rsc-action': id,
},
Expand All @@ -64,7 +70,7 @@ async function main() {
<BrowserRoot />
</React.StrictMode>
)
ReactDOMClient.hydrateRoot(document, browserRoot, {
hydrateRoot(document, browserRoot, {
formState: initialPayload.formState,
})

Expand Down
24 changes: 14 additions & 10 deletions packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import * as ReactServer from '@vitejs/plugin-rsc/rsc'
import {
renderToReadableStream,
createTemporaryReferenceSet,
decodeReply,
loadServerAction,
decodeAction,
decodeFormState,
} from '@vitejs/plugin-rsc/rsc'
import type { ReactFormState } from 'react-dom/client'
import type React from 'react'

Expand Down Expand Up @@ -40,28 +47,25 @@ export async function handleRequest({
const body = contentType?.startsWith('multipart/form-data')
? await request.formData()
: await request.text()
temporaryReferences = ReactServer.createTemporaryReferenceSet()
const args = await ReactServer.decodeReply(body, { temporaryReferences })
const action = await ReactServer.loadServerAction(actionId)
temporaryReferences = createTemporaryReferenceSet()
const args = await decodeReply(body, { temporaryReferences })
const action = await loadServerAction(actionId)
returnValue = await action.apply(null, args)
} else {
// otherwise server function is called via `<form action={...}>`
// before hydration (e.g. when javascript is disabled).
// aka progressive enhancement.
const formData = await request.formData()
const decodedAction = await ReactServer.decodeAction(formData)
const decodedAction = await decodeAction(formData)
const result = await decodedAction()
formState = await ReactServer.decodeFormState(result, formData)
formState = await decodeFormState(result, formData)
}
}

const url = new URL(request.url)
const rscPayload: RscPayload = { root: getRoot(), formState, returnValue }
const rscOptions = { temporaryReferences }
const rscStream = ReactServer.renderToReadableStream<RscPayload>(
rscPayload,
rscOptions,
)
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions)

// respond RSC stream without HTML rendering based on framework's convention.
// here we use request header `content-type`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ReactClient from '@vitejs/plugin-rsc/ssr' // RSC API
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr'
import React from 'react'
import type { ReactFormState } from 'react-dom/client'
import * as ReactDOMServer from 'react-dom/server.edge'
import { renderToReadableStream } from 'react-dom/server.edge'
import { injectRSCPayload } from 'rsc-html-stream/server'
import type { RscPayload } from './entry.rsc'

Expand All @@ -23,7 +23,7 @@ export async function renderHTML(
function SsrRoot() {
// deserialization needs to be kicked off inside ReactDOMServer context
// for ReactDomServer preinit/preloading to work
payload ??= ReactClient.createFromReadableStream<RscPayload>(rscStream1)
payload ??= createFromReadableStream<RscPayload>(rscStream1)
return <FixSsrThenable>{React.use(payload).root}</FixSsrThenable>
}

Expand All @@ -34,7 +34,7 @@ export async function renderHTML(
// render html (traditional SSR)
const bootstrapScriptContent =
await import.meta.viteRsc.loadBootstrapScriptContent('index')
const htmlStream = await ReactDOMServer.renderToReadableStream(<SsrRoot />, {
const htmlStream = await renderToReadableStream(<SsrRoot />, {
bootstrapScriptContent: options?.debugNojs
? undefined
: bootstrapScriptContent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import * as ReactRsc from '@vitejs/plugin-rsc/rsc'
import {
createClientTemporaryReferenceSet,
encodeReply,
createTemporaryReferenceSet,
decodeReply,
renderToReadableStream,
createFromReadableStream,
} from '@vitejs/plugin-rsc/rsc'

// based on
// https://github.com/vercel/next.js/pull/70435
Expand Down Expand Up @@ -28,26 +35,25 @@ export default function cacheWrapper(fn: (...args: any[]) => Promise<unknown>) {
// those arguments to be included as a cache key and it doesn't achieve
// "use cache static shell + dynamic children props" pattern.
// cf. https://nextjs.org/docs/app/api-reference/directives/use-cache#non-serializable-arguments
const clientTemporaryReferences =
ReactRsc.createClientTemporaryReferenceSet()
const encodedArguments = await ReactRsc.encodeReply(args, {
const clientTemporaryReferences = createClientTemporaryReferenceSet()
const encodedArguments = await encodeReply(args, {
temporaryReferences: clientTemporaryReferences,
})
const serializedCacheKey = await replyToCacheKey(encodedArguments)

// cache `fn` result as stream
// (cache value is promise so that it dedupes concurrent async calls)
const entryPromise = (cacheEntries[serializedCacheKey] ??= (async () => {
const temporaryReferences = ReactRsc.createTemporaryReferenceSet()
const decodedArgs = await ReactRsc.decodeReply(encodedArguments, {
const temporaryReferences = createTemporaryReferenceSet()
const decodedArgs = await decodeReply(encodedArguments, {
temporaryReferences,
})

// run the original function
const result = await fn(...decodedArgs)

// serialize result to a ReadableStream
const stream = ReactRsc.renderToReadableStream(result, {
const stream = renderToReadableStream(result, {
environmentName: 'Cache',
temporaryReferences,
})
Expand All @@ -56,7 +62,7 @@ export default function cacheWrapper(fn: (...args: any[]) => Promise<unknown>) {

// deserialized cached stream
const stream = (await entryPromise).get()
const result = ReactRsc.createFromReadableStream(stream, {
const result = createFromReadableStream(stream, {
environmentName: 'Cache',
replayConsoleLogs: true,
temporaryReferences: clientTemporaryReferences,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import * as React from 'react'
import * as ReactDOMClient from 'react-dom/client'
import * as ReactClient from '@vitejs/plugin-rsc/react/browser'
import { createRoot } from 'react-dom/client'
import {
createFromFetch,
setRequireModule,
setServerCallback,
createTemporaryReferenceSet,
encodeReply,
} from '@vitejs/plugin-rsc/react/browser'
import type { RscPayload } from './entry.rsc'

let fetchServer: typeof import('./entry.rsc').fetchServer

export function initialize(options: { fetchServer: typeof fetchServer }) {
fetchServer = options.fetchServer
ReactClient.setRequireModule({
setRequireModule({
load: (id) => import(/* @vite-ignore */ id),
})
}

export async function main() {
let setPayload: (v: RscPayload) => void

const initialPayload = await ReactClient.createFromFetch<RscPayload>(
const initialPayload = await createFromFetch<RscPayload>(
fetchServer(new Request(window.location.href)),
)

Expand All @@ -29,14 +35,14 @@ export async function main() {
return payload.root
}

ReactClient.setServerCallback(async (id, args) => {
setServerCallback(async (id, args) => {
const url = new URL(window.location.href)
const temporaryReferences = ReactClient.createTemporaryReferenceSet()
const payload = await ReactClient.createFromFetch<RscPayload>(
const temporaryReferences = createTemporaryReferenceSet()
const payload = await createFromFetch<RscPayload>(
fetchServer(
new Request(url, {
method: 'POST',
body: await ReactClient.encodeReply(args, { temporaryReferences }),
body: await encodeReply(args, { temporaryReferences }),
headers: {
'x-rsc-action': id,
},
Expand All @@ -53,5 +59,5 @@ export async function main() {
<BrowserRoot />
</React.StrictMode>
)
ReactDOMClient.createRoot(document.body).render(browserRoot)
createRoot(document.body).render(browserRoot)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import * as ReactServer from '@vitejs/plugin-rsc/react/rsc'
import {
setRequireModule,
renderToReadableStream,
createTemporaryReferenceSet,
decodeReply,
loadServerAction,
decodeAction,
decodeFormState,
} from '@vitejs/plugin-rsc/react/rsc'
import type React from 'react'
import { Root } from '../root'
import type { ReactFormState } from 'react-dom/client'
Expand All @@ -12,7 +20,7 @@ export type RscPayload = {
declare let __vite_rsc_raw_import__: (id: string) => Promise<unknown>

export function initialize() {
ReactServer.setRequireModule({ load: (id) => __vite_rsc_raw_import__(id) })
setRequireModule({ load: (id) => __vite_rsc_raw_import__(id) })
}

export async function fetchServer(request: Request): Promise<Response> {
Expand All @@ -27,24 +35,21 @@ export async function fetchServer(request: Request): Promise<Response> {
const body = contentType?.startsWith('multipart/form-data')
? await request.formData()
: await request.text()
temporaryReferences = ReactServer.createTemporaryReferenceSet()
const args = await ReactServer.decodeReply(body, { temporaryReferences })
const action = await ReactServer.loadServerAction(actionId)
temporaryReferences = createTemporaryReferenceSet()
const args = await decodeReply(body, { temporaryReferences })
const action = await loadServerAction(actionId)
returnValue = await action.apply(null, args)
} else {
const formData = await request.formData()
const decodedAction = await ReactServer.decodeAction(formData)
const decodedAction = await decodeAction(formData)
const result = await decodedAction()
formState = await ReactServer.decodeFormState(result, formData)
formState = await decodeFormState(result, formData)
}
}

const rscPayload: RscPayload = { root: <Root />, formState, returnValue }
const rscOptions = { temporaryReferences }
const rscStream = ReactServer.renderToReadableStream<RscPayload>(
rscPayload,
rscOptions,
)
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions)

return new Response(rscStream, {
headers: {
Expand Down
Loading
Loading