Readme

SSR React Mini & SQLite Todo App

This Todo App is server rendered and client-hydrated React. This architecture is a lightweight alternative to NextJS, RemixJS, or other React metaframeworks with no compile or build step. The data is saved server-side in Val Town SQLite.

demo

SSR React Mini Framework

This "framework" is currently 44 lines of code, so it's obviously not a true replacement for NextJS or Remix.

The trick is client-side importing the React component that you're server rendering. Val Town is uniquely suited for this trick because it both runs your code server-side and exposes vals as modules importable by the browser.

The tricky part is making sure that server-only code doesn't run on the client and vice-versa. For example, because this val colocates the server-side loader and action with the React component we have to be careful to do all server-only imports (ie sqlite) dynamically inside the loader and action, so they only run server-side.

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 { zip } from "https://esm.sh/lodash-es";
import { useEffect, useState } from "https://esm.sh/react@18.2.0";
import { Form, hydrate } from "https://esm.town/v/stevekrouse/ssr_hydrate_react";
export async function loader(req: Request) {
const { sqlite } = await import("https://esm.town/v/std/sqlite?v=4");
sqlite.execute(`CREATE TABLE IF NOT EXISTS todos2 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
text TEXT NOT NULL)`);
let { columns, rows } = await sqlite.execute(`select * from todos2`);
const initialTodos = rows.map(row => Object.fromEntries(zip(columns, row)));
return { initialTodos, initialLogs: [`Server rendered`] };
}
export async function action(req: Request) {
const { sqlite } = await import("https://esm.town/v/std/sqlite?v=4");
const formData = await req.formData();
if (req.method === "POST") {
const text = formData.get("text") as string;
const [, { rows }] = await sqlite.batch([{
sql: `insert into todos2(text) values (?)`,
args: [text],
}, "select last_insert_rowid()"]);
return Response.json({ id: rows[0][0], text });
}
else if (req.method === "PUT") {
const id = formData.get("id") as string;
const { rows } = await sqlite.execute({ sql: `select completed_at from todos2 where id = ?`, args: [id] });
const completed_at = rows[0][0];
const new_completed_at = completed_at ? null : new Date() as any;
await sqlite.execute({ sql: `update todos2 set completed_at = ? where id = ?`, args: [new_completed_at, id] });
}
else if (req.method === "DELETE") {
const id = formData.get("id") as string;
await sqlite.execute({ sql: `delete from todos2 where id = ?`, args: [id] });
}
return Response.json("OK");
}
export function Component({ initialTodos, initialLogs }) {
const [todos, setTodos] = useState(initialTodos);
const [logs, setLogs] = useState(initialLogs);
const [newTodo, setNewTodo] = useState("");
const addLog = log => setLogs([...logs, log]);
useEffect(() => addLog(`Client rendered`), []);
function addTodo() {
setTodos([...todos, { text: newTodo }]);
setNewTodo("");
return async (resp: Response) => {
const { text, id } = await resp.json();
setTodos([...todos, { text, id }]); // ok to add again because todos is stale
addLog(`Got ${resp.ok ? "OK" : "Error"} from server for adding todo`);
};
}
function toggleTodo(e) {
const formData = new FormData(e.target);
const id = parseInt(formData.get("id") as string);
setTodos(todos.map(t => t.id === id ? { ...t, completed_at: t.completed_at ? null : Date.now() } : t));
return (resp: Response) => addLog(`Got ${resp.ok ? "OK" : "Error"} from server for toggling todo`);
}
function deleteTodo(e) {
const formData = new FormData(e.target);
const id = parseInt(formData.get("id") as string);
setTodos(todos.filter(t => t.id !== id));
return (resp: Response) => addLog(`Got ${resp.ok ? "OK" : "Error"} from server for deleting todo`);
}
return (
<html>
<head>
<title>Todo List</title>
<script src="https://cdn.tailwindcss.com" />
</head>
<body className="max-w-md mx-auto p-10">
<h2 className="text-xl font-bold pb-2">Todo list</h2>
<ol className="pl-2 max-w-64">
{todos.map((todo) => (
<li key={todo.id} className={`flex justify-between ${todo.completed_at ? "line-through" : ""}`}>
<div className="flex">
<Form action="put" className="mr-2" onSubmit={toggleTodo}>
<input name="id" value={todo.id} type="hidden" />
<button>{todo.completed_at ? "✅" : "⃝"}</button>
</Form>
{todo.text}
</div>
<Form action="delete" onSubmit={deleteTodo}>
<input name="id" value={todo.id} type="hidden" />
<button>x</button>
</Form>
</li>
))}
</ol>
<Form onSubmit={addTodo}>
Only the latest version can be previewed
👆 This is a val. Vals are TypeScript snippets of code, written in the browser and run on our servers. Create scheduled functions, email yourself, and persist small pieces of data — all from the browser.