|
| 1 | +"use client-entry"; |
| 2 | + |
| 3 | +import {useState, use, startTransition, useInsertionEffect, ReactElement} from 'react'; |
| 4 | +import ReactDOM from 'react-dom/client'; |
| 5 | +import {createFromReadableStream, createFromFetch} from 'react-server-dom-parcel/client'; |
| 6 | +import {rscStream} from 'rsc-html-stream/client'; |
| 7 | + |
| 8 | +// Stream in initial RSC payload embedded in the HTML. |
| 9 | +let initialRSCPayload = createFromReadableStream<ReactElement>(rscStream); |
| 10 | +let updateRoot: ((root: ReactElement, cb?: (() => void) | null) => void) | null = null; |
| 11 | + |
| 12 | +function Content() { |
| 13 | + // Store the current root element in state, along with a callback |
| 14 | + // to call once rendering is complete. |
| 15 | + let [[root, cb], setRoot] = useState<[ReactElement, (() => void) | null]>([use(initialRSCPayload), null]); |
| 16 | + updateRoot = (root, cb) => setRoot([root, cb ?? null]); |
| 17 | + useInsertionEffect(() => cb?.()); |
| 18 | + return root; |
| 19 | +} |
| 20 | + |
| 21 | +// Hydrate initial page content. |
| 22 | +startTransition(() => { |
| 23 | + ReactDOM.hydrateRoot(document, <Content />); |
| 24 | +}); |
| 25 | + |
| 26 | +// A very simple router. When we navigate, we'll fetch a new RSC payload from the server, |
| 27 | +// and in a React transition, stream in the new page. Once complete, we'll pushState to |
| 28 | +// update the URL in the browser. |
| 29 | +async function navigate(pathname: string, push = false) { |
| 30 | + let res = fetch(pathname.replace(/\.html$/, '.rsc')); |
| 31 | + let root = await createFromFetch<ReactElement>(res); |
| 32 | + startTransition(() => { |
| 33 | + updateRoot!(root, () => { |
| 34 | + if (push) { |
| 35 | + history.pushState(null, '', pathname); |
| 36 | + push = false; |
| 37 | + } |
| 38 | + }); |
| 39 | + }); |
| 40 | +} |
| 41 | + |
| 42 | +// Intercept link clicks to perform RSC navigation. |
| 43 | +document.addEventListener('click', e => { |
| 44 | + let link = (e.target as Element).closest('a'); |
| 45 | + if ( |
| 46 | + link && |
| 47 | + link instanceof HTMLAnchorElement && |
| 48 | + link.href && |
| 49 | + (!link.target || link.target === '_self') && |
| 50 | + link.origin === location.origin && |
| 51 | + !link.hasAttribute('download') && |
| 52 | + e.button === 0 && // left clicks only |
| 53 | + !e.metaKey && // open in new tab (mac) |
| 54 | + !e.ctrlKey && // open in new tab (windows) |
| 55 | + !e.altKey && // download |
| 56 | + !e.shiftKey && |
| 57 | + !e.defaultPrevented |
| 58 | + ) { |
| 59 | + e.preventDefault(); |
| 60 | + navigate(link.pathname, true); |
| 61 | + } |
| 62 | +}); |
| 63 | + |
| 64 | +// When the user clicks the back button, navigate with RSC. |
| 65 | +window.addEventListener('popstate', e => { |
| 66 | + navigate(location.pathname); |
| 67 | +}); |
0 commit comments