Readme

React SSR and client-side hydration for Val Town

Usage

/** @jsxImportSource https://esm.sh/react */ import { render, React } from "https://esm.town/v/jxnblk/ReactStream"; function App() { const [count, setCount] = React.useState(0); return ( <html> <body> <h1>ReactStream</h1> <p>React SSR with client-side hydration in Val Town</p> <pre>{count}</pre> <button onClick={() => setCount(count - 1)}>-</button> <button onClick={() => setCount(count + 1)}>+</button> </body> </html> ); } export default render(App, import.meta.url);

Live example

To render static HTML without hydration, pass false as the second argument.

export default render(App, false);

Middleware

Custom middleware can be added in an array as the third argument. Middleware can add data to the req.data object or return a response for things like API endpoints.

export default render(App, import.meta.url, [ analytics, robots("User-agent: *\nAllow: /"), getInitialProps ])

robots.txt

ReactStream has a built-in middleware to handle request to /robots.txt

import { render, robots } from "https://esm.town/v/jxnblk/ReactStream"; // ... export default render(App, import.meta.url, [ robots("User-agent: *\nAllow: /"), ])

Add a backend request handler

// example middleware async function api (req: Request, res: Response, next): Promise<Response> { if (req.pathname !== "/api") return next(); if (req.method === "POST") { return Repsonse.json({ message: "Hello POST request" }); } return Response.json({ ok: true }); } export default render(App, import.meta.url, [ api ]);

Fetch data on the server to set initial props

// example middleware async function getInitialProps (req: Request, res: Response, next) { // fetch data or do async work to pass as props to the component req.data = { hello: "props", }; return next(); } export default render(App, import.meta.url, [ getInitialProps ]);

Starter template

/** @jsxImportSource https://esm.sh/react */ import { render } from "https://esm.town/v/jxnblk/ReactStream"; function App () { return ( <html> <head> <title>ReactStream</title> </head> <body> hello </body> </html> ); } export default render(App, import.meta.url, []);

React requires matching versions for SSR and hydration. Import React from https://esm.town/v/jxnblk/ReactStream to ensure your component uses the same version as this library (currently react@18.3.1).

Inspired by https://www.val.town/v/stevekrouse/react_http & https://www.val.town/v/stevekrouse/reactClientDemo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/** @jsxImportSource https://esm.sh/react */
import { hydrateRoot } from "https://esm.sh/react-dom@18.3.1/client";
import * as React from "https://esm.sh/react@18.3.1";
export { React };
export type RequestHandler = (request: Request) => Promise<Response>;
export type DataFetcher<T> = (request: Request) => Promise<T>;
// export type DataRequest<T> = Request & { data: T };
export type DataRequest = Request & { data: any };
export type NextCallback = () => Promise<Response>;
export type Middleware = (req: DataRequest, res: Response, callback: NextCallback) => Promise<Response>;
export interface ReactStreamProps {
url: URL;
pathname: string;
search: string;
searchParams: Record<string, string>;
headers: Headers;
cookie: string;
}
export function render<T>(
/** Root-level React component that renders an entire <html> element
* including the head and body tags.
*/
Component: React.ComponentType<ReactStreamProps>,
/** On Val Town, use `import.meta.url` for client-side hydration */
module: string | false,
/** Optional middleware */
opts: ReactStreamOptions | Middleware[] = [],
) {
const useMiddleware = Array.isArray(opts); // for backwards compat
const options: ReactStreamOptions = !Array.isArray(opts) ? opts : {};
const { api, getInitialProps } = options;
if (typeof document !== "undefined" && module) {
const props = window.__data;
hydrateRoot(document, <Component {...props} />);
}
return async function handler(request: Request): Promise<Response> {
const main = reactStream(Component, module);
const middleware: Middleware[] = [
parseURL,
parseHeaders,
// DEPRECATED (for backwards compat)
options.robots && robots(options.robots),
options.api && deprecatedCustomAPI(options.api),
options.getInitialProps && deprecatedGetInitiaProps(options.getInitialProps),
// New custom middleware
...(useMiddleware ? opts.slice() : []),
main,
].filter(Boolean);
let res: Response = new Response();
const req: DataRequest = Object.assign(request.clone(), { data: {} });
req.data = {};
const next = async (nextRes?: Response): Promise<Response> => {
const fn = middleware.shift();
if (nextRes) res = nextRes;
if (!fn) return main(req, res);
return fn(req, res, next);
};
return next();
};
}
export default render;
// main react response handler
const reactStream = (
Component: React.ComponentType<ReactStreamProps>,
module: string | false,
): Middleware =>
async function(req: DataRequest, res: Response): Promise<Response> {
const { renderToReadableStream } = await import("https://esm.sh/react-dom@18.3.1/server");
const stream = await renderToReadableStream(
<Component {...req.data} />,
module
? {
bootstrapModules: [module],
bootstrapScriptContent: `window.__data=${JSON.stringify(req.data)}`,
}
: {},
);
const headers = res.headers;
headers.set("Content-Type", "text/html");
console.log("react", res.status);
return new Response(stream, {
headers,
status: res.status,
statusText: res.statusText,
});
};
Val Town is a social website to write and deploy JavaScript.
Build APIs and schedule functions from your browser.
Comments
Nobody has commented on this val yet: be the first!
August 14, 2024