A worked example: before and after
A realistic React component: controlled form, a context, a memoized count, an effect with cleanup, a memoized row and a list.
A realistic React component: controlled form, a context, a memoized count, an effect with cleanup, a memoized row and a list.
tsx
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
export const ThemeContext = createContext("light");
type Todo = { id: number; text: string; done: boolean };
const TodoRow = memo(function TodoRow({ todo, onToggle }: { todo: Todo; onToggle: (id: number) => void }) {
return (
<li className={todo.done ? "done" : ""}>
<label>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{todo.text}
</label>
</li>
);
});
export function Todos() {
const theme = useContext(ThemeContext);
const [todos, setTodos] = useState<Todo[]>([]);
const [text, setText] = useState("");
const remaining = useMemo(() => todos.filter((todo) => !todo.done).length, [todos]);
useEffect(() => {
const previous = document.title;
document.title = `${remaining} left`;
return () => {
document.title = previous;
};
}, [remaining]);
const toggle = useCallback((id: number) => {
setTodos((list) => list.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)));
}, []);
return (
<section className={`todos ${theme}`}>
<form
onSubmit={(event) => {
event.preventDefault();
if (text.trim() === "") return;
setTodos((list) => [...list, { id: list.length + 1, text, done: false }]);
setText("");
}}
>
<input aria-label="New todo" value={text} onChange={(event) => setText(event.target.value)} />
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo) => (
<TodoRow key={todo.id} todo={todo} onToggle={toggle} />
))}
</ul>
<p>{remaining} left</p>
</section>
);
}
The ShadowJS version has no memo, no useCallback, no useMemo, no dependency array and no key. The theme
context is a file of its own, as it would be in an app:
src/todo-theme.tsxtsx
import { createContext } from "@shadow-js/core";
export const Theme = createContext("light");
src/todos.tsxtsx
import { useContext, useEffect, useStore } from "@shadow-js/core";
import { For } from "@shadow-js/web";
import { Theme } from "./todo-theme";
interface Todo {
id: number;
text: string;
done: boolean;
}
export function Todos() {
console.log("setup");
const theme = useContext(Theme);
const [todos, setTodos] = useStore<Todo[]>([]);
const [text, setText] = useStore("");
const remaining = todos.filter((todo) => !todo.done).length;
useEffect(() => {
const previous = document.title;
document.title = `${remaining} left`;
return () => {
document.title = previous;
};
});
const toggle = (id: number): void => {
setTodos((todo) => todo.id === id, "done", (done) => !done);
};
return (
<section class={`todos ${theme}`}>
<form
onSubmit={(event) => {
event.preventDefault();
if (text().trim() === "") return;
setTodos((list) => {
list.push({ id: list.length + 1, text: text(), done: false });
});
setText("");
}}
>
<input aria-label="New todo" value={text()} onInput={(event) => setText(event.currentTarget.value)} />
<button type="submit">Add</button>
</form>
<ul>
<For each={todos} key={(todo) => todo.id}>
{(todo) => (
<li class={todo().done ? "done" : ""}>
<label>
<input type="checkbox" checked={todo().done} onChange={() => toggle(todo().id)} />
{todo().text}
</label>
</li>
)}
</For>
</ul>
<p>{remaining} left</p>
</section>
);
}