main
tsx 113 lines 3.09 KB
Raw
1 'use client-entry';
2
3 import {
4 useState,
5 use,
6 startTransition,
7 useInsertionEffect,
8 ReactElement,
9 } from 'react';
10 import ReactDOM from 'react-dom/client';
11 import {
12 createFromReadableStream,
13 createFromFetch,
14 encodeReply,
15 setServerCallback,
16 } from 'react-server-dom-parcel/client';
17 import {rscStream} from 'rsc-html-stream/client';
18
19 // Stream in initial RSC payload embedded in the HTML.
20 let initialRSCPayload = createFromReadableStream<ReactElement>(rscStream);
21 let updateRoot:
22 | ((root: ReactElement, cb?: (() => void) | null) => void)
23 | null = null;
24
25 function Content() {
26 // Store the current root element in state, along with a callback
27 // to call once rendering is complete.
28 let [[root, cb], setRoot] = useState<[ReactElement, (() => void) | null]>([
29 use(initialRSCPayload),
30 null,
31 ]);
32 updateRoot = (root, cb) => setRoot([root, cb ?? null]);
33 useInsertionEffect(() => cb?.());
34 return root;
35 }
36
37 // Hydrate initial page content.
38 startTransition(() => {
39 ReactDOM.hydrateRoot(document, <Content />);
40 });
41
42 // A very simple router. When we navigate, we'll fetch a new RSC payload from the server,
43 // and in a React transition, stream in the new page. Once complete, we'll pushState to
44 // update the URL in the browser.
45 async function navigate(pathname: string, push = false) {
46 let res = fetch(pathname, {
47 headers: {
48 Accept: 'text/x-component',
49 },
50 });
51 let root = await createFromFetch<ReactElement>(res);
52 startTransition(() => {
53 updateRoot!(root, () => {
54 if (push) {
55 history.pushState(null, '', pathname);
56 push = false;
57 }
58 });
59 });
60 }
61
62 // Intercept link clicks to perform RSC navigation.
63 document.addEventListener('click', e => {
64 let link = (e.target as Element).closest('a');
65 if (
66 link &&
67 link instanceof HTMLAnchorElement &&
68 link.href &&
69 (!link.target || link.target === '_self') &&
70 link.origin === location.origin &&
71 !link.hasAttribute('download') &&
72 e.button === 0 && // left clicks only
73 !e.metaKey && // open in new tab (mac)
74 !e.ctrlKey && // open in new tab (windows)
75 !e.altKey && // download
76 !e.shiftKey &&
77 !e.defaultPrevented
78 ) {
79 e.preventDefault();
80 navigate(link.pathname, true);
81 }
82 });
83
84 // When the user clicks the back button, navigate with RSC.
85 window.addEventListener('popstate', e => {
86 navigate(location.pathname);
87 });
88
89 // Intercept HMR window reloads, and do it with RSC instead.
90 window.addEventListener('parcelhmrreload', e => {
91 e.preventDefault();
92 navigate(location.pathname);
93 });
94
95 // Setup a callback to perform server actions.
96 // This sends a POST request to the server, and updates the page with the response.
97 setServerCallback(async function (id: string, args: any[]) {
98 console.log('Handling server action', id, args);
99 const response = fetch(location.pathname, {
100 method: 'POST',
101 headers: {
102 Accept: 'text/x-component',
103 'rsc-action-id': id,
104 },
105 body: await encodeReply(args),
106 });
107 const {result, root} = await createFromFetch<{
108 root: JSX.Element;
109 result: any;
110 }>(response);
111 startTransition(() => updateRoot!(root));
112 return result;
113 });