Skip to content

Commit 87319bf

Browse files
authored
chore(rsc): remove @vite/plugin-rsc/extra API usages from examples (#596)
1 parent c5f0bab commit 87319bf

File tree

12 files changed

+314
-15
lines changed

12 files changed

+314
-15
lines changed

packages/plugin-rsc/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ npx degit vitejs/vite-plugin-react/packages/plugin-rsc/examples/starter my-app
2525
- This demonstrates how to integrate [experimental React Router RSC API](https://remix.run/blog/rsc-preview). React Router now provides [official RSC support](https://reactrouter.com/how-to/react-server-components), so it's recommended to follow React Router's official documentation for the latest integration.
2626
- [`./examples/basic`](./examples/basic)
2727
- This is mainly used for e2e testing and include various advanced RSC usages (e.g. `"use cache"` example).
28-
It also uses a high level `@vitejs/plugin-rsc/extra/{rsc,ssr,browser}` API for quick setup.
2928
- [`./examples/ssg`](./examples/ssg)
3029
- Static site generation (SSG) example with MDX and client components for interactivity.
3130

packages/plugin-rsc/examples/basic/src/client.tsx

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import * as ReactClient from '@vitejs/plugin-rsc/browser'
2+
import { getRscStreamFromHtml } from '@vitejs/plugin-rsc/rsc-html-stream/browser'
3+
import React from 'react'
4+
import * as ReactDOMClient from 'react-dom/client'
5+
import type { RscPayload } from './entry.rsc'
6+
7+
async function main() {
8+
// stash `setPayload` function to trigger re-rendering
9+
// from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr)
10+
let setPayload: (v: RscPayload) => void
11+
12+
// deserialize RSC stream back to React VDOM for CSR
13+
const initialPayload = await ReactClient.createFromReadableStream<RscPayload>(
14+
// initial RSC stream is injected in SSR stream as <script>...FLIGHT_DATA...</script>
15+
getRscStreamFromHtml(),
16+
)
17+
18+
// browser root component to (re-)render RSC payload as state
19+
function BrowserRoot() {
20+
const [payload, setPayload_] = React.useState(initialPayload)
21+
22+
React.useEffect(() => {
23+
setPayload = (v) => React.startTransition(() => setPayload_(v))
24+
}, [setPayload_])
25+
26+
// re-fetch/render on client side navigation
27+
React.useEffect(() => {
28+
return listenNavigation(() => fetchRscPayload())
29+
}, [])
30+
31+
return payload.root
32+
}
33+
34+
// re-fetch RSC and trigger re-rendering
35+
async function fetchRscPayload() {
36+
const payload = await ReactClient.createFromFetch<RscPayload>(
37+
fetch(window.location.href),
38+
)
39+
setPayload(payload)
40+
}
41+
42+
// register a handler which will be internally called by React
43+
// on server function request after hydration.
44+
ReactClient.setServerCallback(async (id, args) => {
45+
const url = new URL(window.location.href)
46+
const temporaryReferences = ReactClient.createTemporaryReferenceSet()
47+
const payload = await ReactClient.createFromFetch<RscPayload>(
48+
fetch(url, {
49+
method: 'POST',
50+
body: await ReactClient.encodeReply(args, { temporaryReferences }),
51+
headers: {
52+
'x-rsc-action': id,
53+
},
54+
}),
55+
{ temporaryReferences },
56+
)
57+
setPayload(payload)
58+
return payload.returnValue
59+
})
60+
61+
// hydration
62+
const browserRoot = (
63+
<React.StrictMode>
64+
<BrowserRoot />
65+
</React.StrictMode>
66+
)
67+
ReactDOMClient.hydrateRoot(document, browserRoot, {
68+
formState: initialPayload.formState,
69+
})
70+
71+
// implement server HMR by trigering re-fetch/render of RSC upon server code change
72+
if (import.meta.hot) {
73+
import.meta.hot.on('rsc:update', () => {
74+
fetchRscPayload()
75+
})
76+
}
77+
}
78+
79+
// a little helper to setup events interception for client side navigation
80+
function listenNavigation(onNavigation: () => void) {
81+
window.addEventListener('popstate', onNavigation)
82+
83+
const oldPushState = window.history.pushState
84+
window.history.pushState = function (...args) {
85+
const res = oldPushState.apply(this, args)
86+
onNavigation()
87+
return res
88+
}
89+
90+
const oldReplaceState = window.history.replaceState
91+
window.history.replaceState = function (...args) {
92+
const res = oldReplaceState.apply(this, args)
93+
onNavigation()
94+
return res
95+
}
96+
97+
function onClick(e: MouseEvent) {
98+
let link = (e.target as Element).closest('a')
99+
if (
100+
link &&
101+
link instanceof HTMLAnchorElement &&
102+
link.href &&
103+
(!link.target || link.target === '_self') &&
104+
link.origin === location.origin &&
105+
!link.hasAttribute('download') &&
106+
e.button === 0 && // left clicks only
107+
!e.metaKey && // open in new tab (mac)
108+
!e.ctrlKey && // open in new tab (windows)
109+
!e.altKey && // download
110+
!e.shiftKey &&
111+
!e.defaultPrevented
112+
) {
113+
e.preventDefault()
114+
history.pushState(null, '', link.href)
115+
}
116+
}
117+
document.addEventListener('click', onClick)
118+
119+
return () => {
120+
document.removeEventListener('click', onClick)
121+
window.removeEventListener('popstate', onNavigation)
122+
window.history.pushState = oldPushState
123+
window.history.replaceState = oldReplaceState
124+
}
125+
}
126+
127+
main()
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import * as ReactServer from '@vitejs/plugin-rsc/rsc'
2+
import type { ReactFormState } from 'react-dom/client'
3+
import type React from 'react'
4+
5+
// The schema of payload which is serialized into RSC stream on rsc environment
6+
// and deserialized on ssr/client environments.
7+
export type RscPayload = {
8+
// this demo renders/serializes/deserizlies entire root html element
9+
// but this mechanism can be changed to render/fetch different parts of components
10+
// based on your own route conventions.
11+
root: React.ReactNode
12+
// server action return value of non-progressive enhancement case
13+
returnValue?: unknown
14+
// server action form state (e.g. useActionState) of progressive enhancement case
15+
formState?: ReactFormState
16+
}
17+
18+
// the plugin by default assumes `rsc` entry having default export of request handler.
19+
// however, how server entries are executed can be customized by registering
20+
// own server handler e.g. `@cloudflare/vite-plugin`.
21+
export async function handleRequest({
22+
request,
23+
getRoot,
24+
nonce,
25+
}: {
26+
request: Request
27+
getRoot: () => React.ReactNode
28+
nonce?: string
29+
}): Promise<Response> {
30+
// handle server function request
31+
const isAction = request.method === 'POST'
32+
let returnValue: unknown | undefined
33+
let formState: ReactFormState | undefined
34+
let temporaryReferences: unknown | undefined
35+
if (isAction) {
36+
// x-rsc-action header exists when action is called via `ReactClient.setServerCallback`.
37+
const actionId = request.headers.get('x-rsc-action')
38+
if (actionId) {
39+
const contentType = request.headers.get('content-type')
40+
const body = contentType?.startsWith('multipart/form-data')
41+
? await request.formData()
42+
: await request.text()
43+
temporaryReferences = ReactServer.createTemporaryReferenceSet()
44+
const args = await ReactServer.decodeReply(body, { temporaryReferences })
45+
const action = await ReactServer.loadServerAction(actionId)
46+
returnValue = await action.apply(null, args)
47+
} else {
48+
// otherwise server function is called via `<form action={...}>`
49+
// before hydration (e.g. when javascript is disabled).
50+
// aka progressive enhancement.
51+
const formData = await request.formData()
52+
const decodedAction = await ReactServer.decodeAction(formData)
53+
const result = await decodedAction()
54+
formState = await ReactServer.decodeFormState(result, formData)
55+
}
56+
}
57+
58+
const url = new URL(request.url)
59+
const rscPayload: RscPayload = { root: getRoot(), formState, returnValue }
60+
const rscOptions = { temporaryReferences }
61+
const rscStream = ReactServer.renderToReadableStream<RscPayload>(
62+
rscPayload,
63+
rscOptions,
64+
)
65+
66+
// respond RSC stream without HTML rendering based on framework's convention.
67+
// here we use request header `content-type`.
68+
// additionally we allow `?__rsc` and `?__html` to easily view payload directly.
69+
const isRscRequest =
70+
(!request.headers.get('accept')?.includes('text/html') &&
71+
!url.searchParams.has('__html')) ||
72+
url.searchParams.has('__rsc')
73+
74+
if (isRscRequest) {
75+
return new Response(rscStream, {
76+
headers: {
77+
'content-type': 'text/x-component;charset=utf-8',
78+
vary: 'accept',
79+
},
80+
})
81+
}
82+
83+
// Delegate to SSR environment for html rendering.
84+
// The plugin provides `loadSsrModule` helper to allow loading SSR environment entry module
85+
// in RSC environment. however this can be customized by implementing own runtime communication
86+
// e.g. `@cloudflare/vite-plugin`'s service binding.
87+
const ssrEntryModule = await import.meta.viteRsc.loadModule<
88+
typeof import('./entry.ssr.tsx')
89+
>('ssr', 'index')
90+
const htmlStream = await ssrEntryModule.renderHTML(rscStream, {
91+
formState,
92+
nonce,
93+
// allow quick simulation of javscript disabled browser
94+
debugNojs: url.searchParams.has('__nojs'),
95+
})
96+
97+
// respond html
98+
return new Response(htmlStream, {
99+
headers: {
100+
'content-type': 'text/html;charset=utf-8',
101+
vary: 'accept',
102+
},
103+
})
104+
}
105+
106+
if (import.meta.hot) {
107+
import.meta.hot.accept()
108+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { injectRscStreamToHtml } from '@vitejs/plugin-rsc/rsc-html-stream/ssr' // helper API
2+
import * as ReactClient from '@vitejs/plugin-rsc/ssr' // RSC API
3+
import React from 'react'
4+
import type { ReactFormState } from 'react-dom/client'
5+
import * as ReactDOMServer from 'react-dom/server.edge'
6+
import type { RscPayload } from './entry.rsc'
7+
8+
export async function renderHTML(
9+
rscStream: ReadableStream<Uint8Array>,
10+
options: {
11+
formState?: ReactFormState
12+
nonce?: string
13+
debugNojs?: boolean
14+
},
15+
) {
16+
// duplicate one RSC stream into two.
17+
// - one for SSR (ReactClient.createFromReadableStream below)
18+
// - another for browser hydration payload by injecting <script>...FLIGHT_DATA...</script>.
19+
const [rscStream1, rscStream2] = rscStream.tee()
20+
21+
// deserialize RSC stream back to React VDOM
22+
let payload: Promise<RscPayload>
23+
function SsrRoot() {
24+
// deserialization needs to be kicked off inside ReactDOMServer context
25+
// for ReactDomServer preinit/preloading to work
26+
payload ??= ReactClient.createFromReadableStream<RscPayload>(rscStream1)
27+
return React.use(payload).root
28+
}
29+
30+
// render html (traditional SSR)
31+
const bootstrapScriptContent =
32+
await import.meta.viteRsc.loadBootstrapScriptContent('index')
33+
const htmlStream = await ReactDOMServer.renderToReadableStream(<SsrRoot />, {
34+
bootstrapScriptContent: options?.debugNojs
35+
? undefined
36+
: bootstrapScriptContent,
37+
nonce: options?.nonce,
38+
// no types
39+
...{ formState: options?.formState },
40+
})
41+
42+
let responseStream: ReadableStream<Uint8Array> = htmlStream
43+
if (!options?.debugNojs) {
44+
// initial RSC stream is injected in HTML stream as <script>...FLIGHT_DATA...</script>
45+
responseStream = responseStream.pipeThrough(
46+
injectRscStreamToHtml(rscStream2, {
47+
nonce: options?.nonce,
48+
}),
49+
)
50+
}
51+
52+
return responseStream
53+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module 'react-dom/server.edge' {
2+
export * from 'react-dom/server'
3+
}

packages/plugin-rsc/examples/basic/src/routes/root.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function Root(props: { url: URL }) {
3434
return (
3535
<html>
3636
<head>
37+
<meta charSet="utf-8" />
3738
<title>vite-rsc</title>
3839
{import.meta.viteRsc.loadCss('/src/routes/root.tsx')}
3940
</head>

packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { revalidateCache } from '../../use-cache-runtime'
1+
import { revalidateCache } from '../../framework/use-cache-runtime'
22

33
export function TestUseCache() {
44
return (

packages/plugin-rsc/examples/basic/src/server.ssr.tsx

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)