Back to packages list

Vals using react

Description from the NPM package:
React is a JavaScript library for building user interfaces.

CORS issues are the bane of frontend engineers.

In Val Town, if you don't customize any CORS headers, we add these defaults:

Access-Control-Allow-Origin: "*"
Access-Control-Allow-Methods: "GET,HEAD,PUT,PATCH,POST,DELETE"

You can override them if you wish to disallow CORS. Check out @neverstew/setCorsHeaders for the easiest way to do this in your code.

This val is a client-side-rendered React app that makes requests to @stevekrouse/cors_example_backend. The backend is in a different val because CORS applies to requests on different domains. The backend has examples of the default permissive CORS behavior and disabled CORS.

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
/** @jsxImportSource https://esm.sh/react */
import { useState } from "https://esm.sh/react@18.2.0";
import react_http from "https://esm.town/v/stevekrouse/react_http";
const BASE_URL = "https://stevekrouse-cors_example_backend.web.val.run";
export function App() {
const [logs, setLogs] = useState([]);
async function request(url, options) {
try {
const response = await fetch(url, options);
const status = response.status;
const data = await response.text();
setLogs([...logs, `${options?.method || "GET"} ${url} [${status}] ${data}`]);
} catch (error) {
setLogs([...logs, `${options?.method || "GET"} ${url} ${error}`]);
}
}
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<h1>CORS Example</h1>
<button onClick={() => request(BASE_URL + "/default", { method: "GET" })}>GET /default</button>
<button onClick={() => request(BASE_URL + "/default", { method: "POST" })}>POST /default</button>
<button onClick={() => request(BASE_URL + "/no_cors", { method: "GET" })}>GET /no_cors</button>
<button onClick={() => request(BASE_URL + "/no_cors", { method: "POST" })}>POST /no_cors</button>
<h2>Logs</h2>
<pre>{logs.join("\n")}</pre>
</div>
);
}
export default react_http(App, import.meta.url);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// JSX can be used in the client val thanks to this magic comment
/** @jsxImportSource https://esm.sh/react **/
// Make sure to only import from esm.sh (npm: specifier are not supported in the browser)
import React from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";
import { Tldraw } from "https://esm.sh/tldraw";
export function App() {
return (
<>
<style type="text/css" src="https://esm.sh/tldraw/tldraw.css"></style>
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw />
</div>
</>
);
}
// The app will be rendered inside the root div
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// JSX can be used in the client val thanks to this magic comment
/** @jsxImportSource https://esm.sh/react **/
// Make sure to only import from esm.sh (npm: specifier are not supported in the browser)
import React from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";
import { Tldraw } from "https://esm.sh/tldraw";
// import "https://esm.sh/tldraw/tldraw.css";
export function App() {
return (
<>
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw />
</div>
</>
);
}
// The app will be rendered inside the root div
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
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
// JSX can be used in the client val thanks to this magic comment
/** @jsxImportSource https://esm.sh/react **/
// Make sure to only import from esm.sh (npm: specifier are not supported in the browser)
import React from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";
import { Button } from "https://esm.town/v/nbbaier/shadcnButton";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "https://esm.town/v/nbbaier/shadcnTable";
const data = [["INV001", "Paid", "Credit Card", "$250.00"], ["INV001", "Paid", "Credit Card", "$250.00"], [
"INV001",
"Paid",
"Credit Card",
"$250.00",
]];
export function App() {
return (
<>
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map(payment => {
return (
<TableRow>
<TableCell className="font-medium">{payment[0]}</TableCell>
<TableCell>{payment[1]}</TableCell>
<TableCell>{payment[2]}</TableCell>
<TableCell className="text-right">{payment[3]}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</>
);
}
// The app will be rendered inside the root div
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

stevekrouse.com - my personal website

This val hosts my personal website. The view data is stored in Val Town SQLite - @std/sqlite.

It used to live on Github Pages, which is why I proxy over requests to certain blog posts over to the Github Pages site still.

Todos

  • Speed up page load by loading sqlite data later like in @healeycodes/steve_web
  • Store more (legally storable) analytics data, and maybe make a sparkline!
  • Add some sort of way to contact me
  • Move over all my blog posts from Github Pages (maybe into @std/blob as a CMS?)
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 { email } from "https://esm.town/v/std/email?v=11";
import { sqlite } from "https://esm.town/v/std/sqlite?v=5";
import { ReloadScriptReactElement } from "https://esm.town/v/stevekrouse/ReloadScript";
import tailwindURL from "https://esm.town/v/stevekrouse/tailwindURL";
import React from "npm:react";
import { renderToString } from "npm:react-dom/server";
const linkClass = "text-blue-500 hover:underline";
const Link = (
{ children, href }: {
children?: React.ReactNode;
href: string;
},
) => <a className={linkClass} href={href}>{children}</a>;
const dateClass = "text-xs text-gray-400 font-mono mr-1 hidden sm:inline-block";
async function getHits() {
const [, , { rows: [[allHits]] }, { rows: [[todayHits]] }] = await sqlite.batch([
"CREATE TABLE IF NOT EXISTS stevekrouse_com_hits (timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)",
"INSERT INTO stevekrouse_com_hits DEFAULT VALUES",
"SELECT COUNT(*) FROM stevekrouse_com_hits where timestamp > datetime('now', '-28 day')",
"SELECT COUNT(*) from stevekrouse_com_hits where timestamp > datetime('now', '-1 day')",
]);
if (allHits % 100 === 0) email({ subject: `You got ${todayHits} hits today! (${allHits} total)` });
return { allHits, todayHits };
}
export default async (request: Request) => {
const url = new URL(request.url);
console.log(url);
if (url.pathname === "/favicon.ico") return new Response(null, { status: 404 });
if (url.pathname !== "/")
return fetch(
`https://stevekrouse.github.io/${url.pathname}${url.search}`,
request as any as RequestInit,
);
const { allHits, todayHits } = await getHits();
return new Response(
renderToString(
<html>
<head>
<title>Steve Krouse</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<script src={tailwindURL} />
{url.searchParams.get("dev")
? <ReloadScriptReactElement vals={[{ valName: "dot_com", userHandle: "stevekrouse" }]} />
: null}
</head>
<body>
<div className="max-w-3xl p-10 space-y-4 mx-auto">
<h1 className="text-orange-600 text-2xl font-medium">Steve Krouse</h1>
<div>
👋 Hi, I'm Steve. I live in Prospect Heights, Brooklyn.
</div>
<div>
I build <Link href="https://val.town">Val Town</Link>, a social website to code in the cloud.
</div>
<div>
This site was{" "}
<Link href="https://www.val.town/v/stevekrouse/dot_com">built in Val Town</Link>. It was viewed{" "}
{todayHits} times today, and {allHits} times this month.
</div>
<div>
<h2 className="text-xl mb-1">Projects</h2>
<ul>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Jul -</span>
<Link href="https://val.town">Val Town</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Sep -</span>
<Link href="https://dateme.directory">Date Me Directory</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2022 Jan -</span>
<Link href="https://twitter.com/stevekrouse/status/1520162279899078657">Zaplib</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2021 Mar -</span>
<Link href="http://updates.compose.run">Compose</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2017 Jul -</span>
<Link href="https://futureofcoding.org/">Future of Coding</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2016 May -</span>
<Link href="https://github.com/stevekrouse/woofjs">WoofJS</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2015 Sep -</span>
<Link href="http://coding.space">The Coding Space Curriculum</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2015 Jul -</span>
<Link href="http://thecodingspace.com">The Coding Space</Link>
</li>
<li className="pb-2 sm:pb-1">
<span className={dateClass}>2014 Jan -</span>Software Engineer @{" "}
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
// JSX can be used in the client val thanks to this magic comment
/** @jsxImportSource https://esm.sh/react **/
// Make sure to only import from esm.sh (npm: specifier are not supported in the browser)
import React from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";
import { Button } from "https://esm.town/v/nbbaier/shadcnButton";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "https://esm.town/v/nbbaier/shadcnTable";
const data = [["INV001", "Paid", "Credit Card", "$250.00"], ["INV001", "Paid", "Credit Card", "$250.00"], [
"INV001",
"Paid",
"Credit Card",
"$250.00",
]];
export function App() {
return (
<>
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map(payment => {
return (
<TableRow>
<TableCell className="font-medium">{payment[0]}</TableCell>
<TableCell>{payment[1]}</TableCell>
<TableCell>{payment[2]}</TableCell>
<TableCell className="text-right">{payment[3]}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</>
);
}
// The app will be rendered inside the root div
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
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
// JSX can be used in the client val thanks to this magic comment
/** @jsxImportSource https://esm.sh/react **/
// Make sure to only import from esm.sh (npm: specifier are not supported in the browser)
import React from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";
import { Button } from "https://esm.town/v/nbbaier/shadcnButton";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "https://esm.town/v/nbbaier/shadcnTable";
const data = [["INV001", "Paid", "Credit Card", "$250.00"], ["INV001", "Paid", "Credit Card", "$250.00"], [
"INV001",
"Paid",
"Credit Card",
"$250.00",
]];
export function App() {
return (
<>
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map(payment => {
return (
<TableRow>
<TableCell className="font-medium">{payment[0]}</TableCell>
<TableCell>{payment[1]}</TableCell>
<TableCell>{payment[2]}</TableCell>
<TableCell className="text-right">{payment[3]}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</>
);
}
// The app will be rendered inside the root div
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
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
// JSX can be used in the client val thanks to this magic comment
/** @jsxImportSource https://esm.sh/react **/
// Make sure to only import from esm.sh (npm: specifier are not supported in the browser)
import React from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";
function Counter() {
const [counter, setCounter] = React.useState(0);
return (
<div>
<span>{counter}</span>
<div>
<button onClick={() => setCounter(count => count - 1)}>-</button>
<button onClick={() => setCounter(count => count + 1)}>+</button>
</div>
</div>
);
}
export function App() {
return (
<>
<h1>React Example</h1>
<Counter />
</>
);
}
// The app will be rendered inside the root div
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

Live reload in new tabs

When you're working on an HTML HTTP val in a new tab, it's annoying to have to manually reload the tab on every save. In the Val Town editor, you can hit cmd+enter, but there's nothing like that for a val in a new tab because Val Town doesn't control that new tab (like we control the iframe in the browser preview). However, you control that HTML via the fetch handler you're writing, so you can add a script that polls the Val Town API for the current version number of your val, and reload itself if it detects a new version. This val has a collection of helpers to help you do just that.

Usage

Create valimport { html } from "https://esm.town/v/stevekrouse/html"; import { reloadOnSaveFetchMiddleware } from "https://esm.town/v/stevekrouse/reloadOnSave"; export default reloadOnSaveFetchMiddleware(async function(req: Request): Promise<Response> { return html(`<h1>Hello!!</h1>`); })
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
import { createElement } from "https://esm.sh/react";
import { InjectHTMLElementStream } from "https://esm.town/v/andreterron/InjectHTMLElementStream?v=9";
import { rootValRef } from "https://esm.town/v/andreterron/rootValRef?v=3";
import { ValRef } from "https://esm.town/v/andreterron/ValRef?v=1";
import { getCurrentValVersionNumber } from "https://esm.town/v/stevekrouse/getCurrentValVersionNumber";
import { modifyResponse } from "https://esm.town/v/stevekrouse/modifyResponse";
import { parentReference } from "https://esm.town/v/stevekrouse/parentReference";
import type { MiddlewareHandler } from "npm:hono";
// this script runs client-side
export const reloadOnVals = async function(vals: ValRef[]) {
const valVersions = await Promise.all(vals.map(getCurrentValVersionNumber));
// console.log("initialValVersions: ", valVersions);
const interval = setInterval(async () => {
let newValVersions = await Promise.all(vals.map(getCurrentValVersionNumber));
// console.log("newValVersions: ", newValVersions);
if (JSON.stringify(newValVersions) !== JSON.stringify(valVersions)) {
clearInterval(interval);
window.location.reload();
}
}, 1000);
};
// experimental - don't use this
export function reloadOnSaveHonoMiddleware(vals: ValRef[] = [rootValRef()]): MiddlewareHandler {
return async function(c, next) {
await next();
// don't reload on custom domains, only reload on val.run URLs
if (!new URL(c.req.url).hostname.includes("val.run")) {
return;
}
c.res = modifyResponse(
c.res,
new InjectHTMLElementStream(ReloadScriptText(vals)),
);
};
}
/**
* @param handler http val's fetch handler
* @param vals to watch
*/
export function reloadOnSaveFetchMiddleware(
handler: (req: Request) => Response | Promise<Response>,
vals = [rootValRef()],
): (req: Request) => Promise<Response> {
return async (req: Request): Promise<Response> => {
const res = await handler(req);
// don't reload on custom domains, only reload on val.run URLs
if (!new URL(req.url).hostname.includes("val.run")) {
return res;
}
return modifyResponse(
res,
new InjectHTMLElementStream(ReloadScriptText(vals)),
);
};
}
// generates a script element as a string
export const ReloadScriptText = (vals = [rootValRef()]) =>
`<script type="module">
import { reloadOnVals } from "${import.meta.url}"
reloadOnVals(${JSON.stringify(vals)})
</script>`;
// generates a react element
export const ReloadScriptReactElement = ({ vals } = { vals: [rootValRef()] }) =>
createElement("script", {
type: "module",
dangerouslySetInnerHTML: {
__html: `import { reloadOnVals } from "${import.meta.url}";
reloadOnVals(${JSON.stringify(vals)});`,
},
});

SSR + Hydration Demo

Look at @pomdtr/island and @pomdtr/hydrate_islands to read the whole library source code (less than 50 rows!).

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
/** @jsxImportSource https://esm.sh/react */
import { useEffect, useState } from "https://esm.sh/react";
// will run on both client and server
export function Demo(props: { items: string[] }) {
const [items, setItems] = useState(props.items);
useEffect(() => {
items.push("This item is rendered only on the client, after the hydration");
setItems([...items]);
// fetch data from the server
fetch("/api/item")
.then((res) => res.json())
.then((item) => setItems([...items, item]));
}, []);
return (
<div className="flex h-full justify-center items-center font-mono">
<ol className="list-decimal">
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ol>
</div>
);
}
// You server code should be contained in this function
export default async function (req: Request) {
const url = new URL(req.url);
if (url.pathname == "/api/item") {
return Response.json("This item is fetched from the server by the client");
}
// server-side import should only be used here
const { Island } = await import("https://esm.town/v/pomdtr/island");
const { renderToString } = await import("https://esm.sh/react-dom/server");
return new Response(
renderToString(
<html>
<head>
<script src="https://cdn.tailwindcss.com"></script>
<script
src="https://esm.town/v/pomdtr/hydrate_islands"
type="module"
defer
></script>
</head>
<body className="h-screen">
<Island src={import.meta.url} name={Demo.name}>
<Demo
items={[
"This item is rendered both on the server and the client",
]}
/>
</Island>
</body>
</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
/** @jsxImportSource https://esm.sh/react */
import React, { useState } from "https://esm.sh/react";
type Props = {
initialCount?: Number;
};
export default function Counter(props: Props) {
const [count, setCount] = useState(props.initialCount || 0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-l"
onClick={decrement}
>
-
</button>
<span className="bg-white border border-blue-500 py-2 px-4">{count}</span>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-r"
onClick={increment}
>
+
</button>
</div>
);
}
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
/** @jsxImportSource https://esm.sh/react */
import React from "https://esm.sh/react";
export function Island({
src,
name,
children,
}: {
src: string;
name: string;
children: any;
}) {
const child = React.Children.only(children);
return (
<div
data-hydration-src={src}
data-hydration-name={name}
data-hydration-props={JSON.stringify(child.props)}
>
{child}
</div>
);
}

⚠️ Deprecated in favor of @stevekrouse/reloadOnSave

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
import { createElement } from "https://esm.sh/react";
import { getCurrentValVersionNumber } from "https://esm.town/v/stevekrouse/getCurrentValVersionNumber?v=4";
import { parentReference } from "https://esm.town/v/stevekrouse/parentReference";
export const reloadOnVals = async function(vals: { userHandle: string; valName: string }[]) {
const valVersions = await Promise.all(vals.map(getCurrentValVersionNumber));
// console.log("initialValVersions: ", valVersions);
const interval = setInterval(async () => {
let newValVersions = await Promise.all(vals.map(getCurrentValVersionNumber));
// console.log("newValVersions: ", newValVersions);
if (JSON.stringify(newValVersions) !== JSON.stringify(valVersions)) {
clearInterval(interval);
window.location.reload();
}
}, 1000);
};
export const ReloadScriptText = (vals = [parentReference()]) =>
`<script type="module">
import { reloadOnVals } from "${import.meta.url}"
reloadOnVals(${JSON.stringify(vals)})
</script>`;
export const ReloadScriptReactElement = ({ vals } = { vals: [parentReference()] }) =>
createElement("script", {
type: "module",
dangerouslySetInnerHTML: {
__html: `import { reloadOnVals } from "${import.meta.url}";
reloadOnVals(${JSON.stringify(vals)});`,
},
});

Starter App for ssr_react_mini

You need to export four things:

  1. loader - runs on any GET request, on the server. it accepts the Request and returns the props of your React compnent.
  2. action- runs on the server on any non-GET, ie POST, PUT, DELETE, or <form>s submit
  3. Component - your React component. it's initially server-rendered and then client-hydrated
  4. default - you should mostly leave this line alone

This is framework is bleeding-edge. You'll need to read the code of the framework itself (it's very short) to understand what it's doing.

If you have questions or comments, please comment below on this val! (or any of these vals)

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
/** @jsxImportSource https://esm.sh/react */
import { useEffect, useState } from "https://esm.sh/react@18.2.0";
import codeOnValTown from "https://esm.town/v/andreterron/codeOnValTown?v=46";
import { Button, Form, hydrate } from "https://esm.town/v/stevekrouse/ssr_react_mini";
// runs on page load, on the server
export async function loader(req: Request) {
// ensure any server-side imports only run server side
const { sqlite } = await import("https://esm.town/v/std/sqlite?v=4");
const [, { columns, rows }] = await sqlite.batch([
// you can optionally create your table here IF NOT EXISTS
`CREATE TABLE IF NOT EXISTS ssr_react_mini_startesr_clicks (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
// get the data your page needs on load
`select count(*) from ssr_react_mini_starter_clicks`,
]);
// return the props for your Component
return { initialClicks: rows[0][0] };
}
// handle <Form> submissions and other server-requests
export async function action(req: Request) {
const { sqlite } = await import("https://esm.town/v/std/sqlite?v=4");
if (req.method === "POST") {
await sqlite.execute(`INSERT INTO ssr_react_mini_starter_clicks DEFAULT VALUES`);
}
return Response.json("OK");
}
export function Component({ initialClicks }: { initialClicks: number }) {
const [clicks, setClicks] = useState(0);
const addClick = () => setClicks(clicks + 1); // optimistic, client-side
return (
<html>
<head>
<title>SSR React Starter</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com" />
</head>
<body className="max-w-md mx-auto p-10">
<h1 className="text-3xl font-bold">Hello Val Town</h1>
<div className="flex justify-between">
<div>
<div>{clicks + initialClicks} clicks ever</div>
<div>{clicks} clicks since the page loaded</div>
</div>
<Form onSubmit={addClick}>
<Button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Click!</Button>
</Form>
</div>
</body>
</html>
);
}
export default codeOnValTown(hydrate(import.meta.url));
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
/** @jsxImportSource https://esm.sh/react */
import { useEffect, useState } from "https://esm.sh/react";
export default function Component() {
const [text, setText] = useState("from server");
useEffect(() => {
console.log("from client");
// Set up an interval to update the textarea every second
const intervalId = setInterval(() => {
const currentTime = new Date().toLocaleTimeString();
setText(currentTime);
}, 1000);
// Clean up the interval on component unmount
return () => clearInterval(intervalId);
}, []);
return (
<div>
<p>{text}</p>
</div>
);
}