Back to packages list

Vals using react-dom

Description from the NPM package:
React package for working with the DOM.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** @jsxImportSource npm:react **/
import { renderToString } from "npm:react-dom@18/server";
export default (req: Request) => {
return new Response(
renderToString(
<html>
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.1" />
<main>
<h1>Hello kirineko!</h1>
<p>This is your first val. hello world!</p>
</main>
</html>,
),
{ headers: { "Content-Type": "text/html" } },
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default (component: Function, url: string) =>
function(req: Request) {
return new Response(
`<html>
<body></body>
<script type="module">
import { ${component.name} } from "${url}"
import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
import { jsx } from "https://esm.sh/react@18.2.0/jsx-runtime";
createRoot(document.body).render(jsx(App, {}));
</script>
</html>`,
{
headers: {
"content-type": "text/html",
},
},
);
};

React SSR and client-side hydration for Val Town

Usage

Create val/** @jsxImportSource https://esm.sh/react */ import resrv, { React } from "https://esm.town/v/jxnblk/resrv"; function App() { const [count, setCount] = React.useState(0); return ( <div> <h1>Resrv</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> </div> ); } export default resrv(App, import.meta.url);

Live example

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

HTML Root Hydration

To render a component that includes a <head> and <body> tag, pass root: true to the third options argument:

Create valfunction App ({ script }) { return ( <body> <h1>Hello</h1> {script} </body> ); } export default resrv(App, import.meta.url, { root: true });

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

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
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "https://esm.sh/react-dom@18.3.1/server";
import * as React from "https://esm.sh/react@18.3.1";
// API:
// import resrv, { React } from "https://esm.town/v/jxnblk/resrv";
// export default resrv(Component, import.meta.url);
export { React };
const createScript = (module: string) => `
import App from "${module}";
import { hydrateRoot } from "https://esm.sh/react-dom@18.3.1/client";
import { jsx } from "https://esm.sh/react@18.3.1/jsx-runtime";
const root = document.getElementById("root");
hydrateRoot(root, jsx(App, {}));
`;
const App = ({ Component, module, simple }: {
Component: any;
module: string;
simple?: boolean;
}) => {
const script = (
<script
type="module"
dangerouslySetInnerHTML={{ __html: createScript(module) }}
/>
);
if (simple) {
return (
<>
<head></head>
<body id="root">
<Component />
{script}
</body>
</>
);
}
return <Component script={script} />;
};
interface Options {
root?: boolean;
}
export default function resrv(Component, module: string, opts: Options = {}) {
return function<T extends object>(args: Request | T): Response | any {
if (args instanceof Request) {
const body = renderToString(<App Component={Component} module={module} simple={!opts.root} />);
const html = `<!DOCTYPE html>
<html lang="en" ${opts.root ? "id='root'" : ""}>
${body}
</html>`;
return new Response(html, {
headers: {
"Content-Type": "text/html",
},
});
}
return <App Component={Component} module={module} />;
};
}

Client Side React Helper

Example Usage

Create val/** @jsxImportSource https://esm.sh/react */ import react_http from "https://esm.town/v/stevekrouse/react_http?v=6"; import { useState } from "https://esm.sh/react@18.2.0"; export function App() { const [ count, setCount ] = useState(0) return ( <div> <h1>Example App</h1> </div> } export default () => react_http({ component: App, sourceURL: import.meta.url, head: `<meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://cdn.tailwindcss.com"></script> <title>Example App</title>` })

Gotchas

Only use https imports

The val with your React component will be imported in the browser. Thus, only use https imports in this val and any that it imports. Replace any npm: with https://esm.sh/ and everything should work great.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function({ component, sourceURL, head }: { component: Function; sourceURL: string; head?: string }) {
return new Response(
`<html>
<head>${head || ""}</head>
<body></body>
<script type="module">
import { ${component.name} } from "${sourceURL}";
import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
import { jsx } from "https://esm.sh/react@18.2.0/jsx-runtime";
createRoot(document.body).render(jsx(${component.name}, {}));
</script>
</html>`,
{
headers: {
"content-type": "text/html",
},
},
);
}

Server-side Render React Mini Framework

This is very experimental, more of a prototype of an architecture, than a true framework

Example: https://www.val.town/v/stevekrouse/TodoApp

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
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "https://esm.sh/react-dom@18.2.0/server";
import { useEffect, useState } from "https://esm.sh/react@18.2.0";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=25";
import { html } from "https://esm.town/v/stevekrouse/html";
// button that's disabled until client react hydrates
export const Button = (props) => {
const [clientHydrated, setClientHydrated] = useState(false);
useEffect(() => setClientHydrated(true), []);
return <button disabled={!clientHydrated} {...props}></button>;
};
export const Form = ({ onSubmit, ...props }: any) => {
const [clientHydrated, setClientHydrated] = useState(false);
useEffect(() => setClientHydrated(true), []);
return (
<form
disabled={!clientHydrated}
onSubmit={async (e) => {
e.preventDefault();
const onData = onSubmit ? onSubmit(e) : () => 1;
const formData = new FormData(e.target as any);
const resp = await fetch("/", {
method: props.action ?? "POST",
body: formData,
});
await onData(resp);
}}
{...props}
>
</form>
);
};
export const hydrate = (importMetaURL: string) =>
async function(req: Request): Promise<Response> {
const { author, name } = extractValInfo(importMetaURL);
const valURL = `https://www.val.town/v/${author}/${name}`;
const moduleURL = `https://esm.town/v/${author}/${name}`;
const exports = await import(moduleURL);
const action = exports.action ?? (() => ({}));
const loader = exports.loader ?? (() => ({}));
const Component = exports.Component ?? (() => (
<div>
Error: Please ensure you <code>export Component</code>" in{" "}
<a target="_blank" href={valURL}>@{author}/{name}</a>.
</div>
));
if (req.method === "GET") {
const props = await loader(req);
const script = `
import { hydrateRoot } from "https://esm.sh/react-dom@18.2.0/client";
import { jsx as _jsx } from "https://esm.sh/react@18.2.0/jsx-runtime";
import { Component } from "https://esm.town/v/${author}/${name}";
let props = ${JSON.stringify(props)}
try {
hydrateRoot(document, _jsx(Component, props));
} catch (e) {
console.error(e)
}
`;
return html(renderToString(
<>
<Component {...props} />
<script type="module" dangerouslySetInnerHTML={{ __html: script }} />
</>,
));
} else {
return action(req);
}
};
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
/** @jsxImportSource npm:react **/
import { renderToString } from "npm:react-dom@18/server";
export default (req: Request) => {
const styles = `
@keyframes rainbow-text {
0% { color: red; }
15% { color: orange; }
30% { color: yellow; }
45% { color: green; }
60% { color: blue; }
75% { color: indigo; }
90% { color: violet; }
100% { color: red; }
}
.rainbow-letter {
display: inline-block;
animation: rainbow-text 10s linear infinite;
}
.rainbow-text-container {
white-space: pre; /* This preserves whitespace, including spaces */
}
`;
const text = "It me, Karlstens - In Val Town!";
const rainbowText = text.split("").map((letter, index) => (
<span
className="rainbow-letter"
style={{ animationDelay: `${-index * 0.75}s` }}
>
{letter}
</span>
));
return new Response(
renderToString(
<html>
<style>{styles}</style>
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.1" />
<main>
<h1 className="rainbow-text-container">{rainbowText}</h1>
<p>Behold, the power of rainbows. 🌈</p>
</main>
</html>,
),
{ headers: { "Content-Type": "text/html" } },
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/** @jsxImportSource npm:react **/
import { renderToString } from "npm:react-dom@18/server";
export default (req: Request) => {
return new Response(
renderToString(
<html>
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.1" />
<main>
<h1>Hello westbrookdaniel!</h1>
<p>This is your first val. It's pretty quick?</p>
</main>
</html>,
),
{ headers: { "Content-Type": "text/html" } },
);
};
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
/** @jsxImportSource https://esm.sh/react */
import ReactDom from "https://esm.sh/react-dom";
import { createStore } from "https://esm.sh/tinybase";
import { createRemotePersister } from "https://esm.sh/tinybase/persisters/persister-remote";
import { useValue } from "https://esm.sh/tinybase/ui-react";
// The store is automatically persisted on the remote server
const store = createStore();
const persister = createRemotePersister(store, `${window.location.origin}/load`, `${window.location.origin}/save`);
await persister.startAutoLoad({}, {
count: 0,
});
await persister.startAutoSave();
function App() {
// The component will be refreshed each time the store is updated
const count = useValue("count", store);
return (
<main>
<h1>Tinybase Example</h1>
<p>Open this website in two tabs to see the values get synced after a short interval.</p>
<div>
<button
onClick={() => {
// This will triggers an update immediately, then send the result to the server
store.setValue("count", count - 1);
}}
>
-
</button>
<span>{count}</span>
<button
onClick={() => {
store.setValue("count", count + 1);
}}
>
+
</button>
</div>
</main>
);
}
const root = ReactDom.createRoot(document.body);
root.render(<App />);
1
2
3
4
5
6
7
8
9
10
11
12
const island = document.querySelector("[data-hydration-src]");
if (!island) {
throw new Error("Could not find island");
}
const { hydrateRoot } = await import("https://esm.sh/react-dom@18.2.0/client");
const { jsx: _jsx } = await import("https://esm.sh/react@18.2.0/jsx-runtime");
const src = island.getAttribute("data-hydration-src");
console.log(`hydrating ${src}`);
const { Component } = await import(src);
const props = JSON.parse(island.getAttribute("data-hydration-props"));
hydrateRoot(island, _jsx(Component, props));

Demo: @pomdtr/example_ssr

Todo:

  • only hydrate island when they enter the viewport
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const islands = Array.from(document.querySelectorAll("[data-hydration-src]"));
if (islands.length > 0) {
const { hydrateRoot } = await import("https://esm.sh/react-dom@18.2.0/client");
const { jsx: _jsx } = await import("https://esm.sh/react@18.2.0/jsx-runtime");
for (const island of islands) {
let src = island.getAttribute("data-hydration-src");
if (!src.startsWith("https://")) {
src = `https://esm.town/v/${src}`;
}
let name = island.getAttribute("data-hydration-name");
if (!name) {
name = "default";
}
const mod = await import(src);
const Component = mod[name];
if (!Component) {
throw new Error(`Component ${name} is not exported`);
}
const props = JSON.parse(island.getAttribute("data-hydration-props"));
hydrateRoot(island, _jsx(Component, props));
}
}

Todo

  • extract params from request paths using urlpatterns
  • add a Form component
  • add support for Layouts
  • support other types url in router ("https://val.town/v/user/val" or "owner/val")
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
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "https://esm.sh/react-dom@18.2.0/server";
import { html } from "https://esm.town/v/stevekrouse/html";
export type PageProps<T = {}> = T & {
route: string;
params: Record<string, string>;
};
type Context = PageProps & {
render: (props) => Response | Promise<Response>;
};
export type Action = (req: Request, ctx: Context) => Response | Promise<Response>;
export function handler(Component, action?: Action) {
return async (c) => {
if (action) {
return action(c.req.raw, {
params: c.req.param(),
route: c.req.routePath,
render(props) {
return html(renderToString(
<Component
{...{
params: c.req.param(),
route: c.req.routePath,
...props,
}}
/>,
));
},
});
}
return html(renderToString(
<Component
{...{
params: c.req.param(),
route: c.req.routePath,
}}
/>,
));
};
}

Server-side Render React Mini Framework

This is very experimental, more of a prototype of an architecture, than a true framework

Example: https://www.val.town/v/stevekrouse/TodoApp

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
/** @jsxImportSource https://esm.sh/react */
import { renderToString } from "https://esm.sh/react-dom@18.2.0/server";
import { useEffect, useState } from "https://esm.sh/react@18.2.0";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=25";
import { html } from "https://esm.town/v/stevekrouse/html";
// button that's disabled until client react hydrates
export const Button = (props) => {
const [clientHydrated, setClientHydrated] = useState(false);
useEffect(() => setClientHydrated(true), []);
return <button disabled={!clientHydrated} {...props}></button>;
};
export const Form = ({ onSubmit, ...props }: any) => {
const [clientHydrated, setClientHydrated] = useState(false);
useEffect(() => setClientHydrated(true), []);
return (
<form
disabled={!clientHydrated}
onSubmit={async (e) => {
e.preventDefault();
const onData = onSubmit ? onSubmit(e) : () => 1;
const formData = new FormData(e.target as any);
const resp = await fetch("/", {
method: props.action ?? "POST",
body: formData,
});
await onData(resp);
}}
{...props}
>
</form>
);
};
export const hydrate = (importMetaURL: string) =>
async function(req: Request): Promise<Response> {
const { author, name } = extractValInfo(importMetaURL);
const valURL = `https://www.val.town/v/${author}/${name}`;
const moduleURL = `https://esm.town/v/${author}/${name}`;
const exports = await import(moduleURL);
const action = exports.action ?? (() => ({}));
const loader = exports.loader ?? (() => ({}));
const Component = exports.Component ?? (() => (
<div>
Error: Please ensure you <code>export Component</code>" in{" "}
<a target="_blank" href={valURL}>@{author}/{name}</a>.
</div>
));
if (req.method === "GET") {
const props = await loader(req);
const script = `
import { hydrateRoot } from "https://esm.sh/react-dom@18.2.0/client";
import { jsx as _jsx } from "https://esm.sh/react@18.2.0/jsx-runtime";
import { Component } from "https://esm.town/v/${author}/${name}";
let props = ${JSON.stringify(props)}
try {
hydrateRoot(document, _jsx(Component, props));
} catch (e) {
console.error(e)
}
`;
return html(renderToString(
<>
<Component {...props} />
<script type="module" dangerouslySetInnerHTML={{ __html: script }} />
</>,
));
} else {
return action(req);
}
};
1
2
3
4
5
6
7
import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
import { jsx as _jsx } from "https://esm.sh/react/jsx-runtime";
import { TodoApp } from "https://esm.town/v/stevekrouse/TodoApp";
const root = createRoot(document.getElementById("app"));
let initialTodos = await fetch("https://stevekrouse-todo_list.web.val.run/todos").then(r => r.json());
root.render(_jsx(TodoApp, { initialTodos: initialTodos }));
1
Next