2. Write the first component
Pass props to a component, keep a todo in a store and update one checkbox and one class when it changes.
Goal: a TodoItem component that shows one todo, and an App that owns the todo's state and toggles it.
See State with useStore for the full state API.
The data#
Start with the shape of a todo. It is plain data; the framework does not care how you model it.
export interface Todo {
id: number;
title: string;
done: boolean;
}
A component that receives props#
A component is a function of one argument, props. Each property of props is a getter, so reading
props.done inside JSX re-reads whatever the parent passed. That is how a child updates without being called
again.
export interface TodoItemProps {
title: string;
done: boolean;
onToggle: () => void;
}
export function TodoItem(props: TodoItemProps) {
return (
<li classList={{ done: props.done }}>
<label>
<input type="checkbox" checked={props.done} onChange={() => props.onToggle()} />
{props.title}
</label>
</li>
);
}
Three things to notice:
classList={{ done: props.done }}toggles the classdonewheneverprops.donechanges.class="..."accepts a string, array or object too.checked={props.done}is a binding: the compiler wraps the expression so that only this attribute is rewritten whenprops.donechanges.onChangeis an event prop. Handlers are bound once; ShadowJS delegates common events (click,input,change,keydown...) to the document.
The function body runs once, when the component is created. After that only the three bindings above (the
class, the checked property and the title text) are kept up to date.
State with useStore#
App owns the todo. useStore with a plain object creates a deep store: reading todo.done subscribes to
exactly that property, and the setter changes exactly what you name.
import { useStore } from "@shadow-js/core";
import { TodoItem } from "./TodoItem";
import type { Todo } from "./types";
export function App() {
const [todo, setTodo] = useStore<Todo>({ id: 1, title: "Learn ShadowJS", done: false });
return (
<main>
<h1>Todos</h1>
<ul>
<TodoItem
title={todo.title}
done={todo.done}
onToggle={() => setTodo("done", (done) => !done)}
/>
</ul>
</main>
);
}
useStore has two shapes, decided by what you pass:
| Call | Returns | Read | Write |
|---|---|---|---|
useStore(0) |
[read, write] |
count() |
setCount(5), setCount((n) => n + 1) |
useStore({ ... }) or useStore([ ... ]) |
[store, setStore] |
todo.done (property access) |
setTodo("done", true), setTodo((draft) => { ... }) |
A value that can be null or change type (Todo | null) is a single value too: useStore<Todo | null>(null)
returns [read, write], you replace it as a whole, and plain data assigned to it is still read fine-grained.
What you should see#
A “Todos” heading and one unchecked “Learn ShadowJS” item appear. Click the checkbox: the item becomes checked
and gets the done class. Click again to clear it. The store updates both bindings without calling either
component function again.
The last assertion shows that the same li element is still in the document: nothing was re-created, the class
was changed in place.
Next: 3. Render a list.