useState becomes useStore
useState becomes useStore in the React to ShadowJS guide.
useStore(initial) is the default. Pass a single value and you get an accessor and a setter. Pass plain
objects or arrays and you get a deep, fine-grained store: reading state.user.name subscribes to exactly that
property, and writing notifies exactly its readers. There is no second state hook to choose from.
import { useStore } from "@shadow-js/core";
interface Book {
title: string;
read: boolean;
}
export function Profile() {
const [name] = useStore("Ada");
const [shelf] = useStore({
books: [
{ title: "Notes", read: true },
{ title: "Letters", read: false },
] as Book[],
});
const [current] = useStore<Book | null>(null);
const readCount = shelf.books.filter((book) => book.read).length;
return (
<p>
{name()} reads {readCount} of {shelf.books.length}
{current() === null ? "" : ` (${current()?.title})`}
</p>
);
}
Choose between them like this:
| You have | Use | Why |
|---|---|---|
| A number, string, boolean | useStore(0) |
Returns [Accessor, Setter] |
| Plain objects or arrays | useStore({ ... }) |
Returns [store, setStore]; property reads are tracked per key |
A value that can be null or change type (Todo[] or null) |
useStore |
A union is replaced as a whole through the setter; assigned data is read fine-grained |
Date, Map, Set, class instances, functions |
Either, as a single value | Only plain objects and arrays are wrapped |
A custom equality, or equals: false to notify on every write |
useSignal(value, { equals }) |
Setters take a value or an updater, as in React. A function can only be stored as a value by wrapping it:
set(() => fn). A store setter takes a draft function, a partial object, a reconcile() result, or a path.
Three differences from React state matter when you port an object:
- A partial object is merged, not replaced.
setState({ count: 5 })changescountand keeps the other keys; React'ssetState({ ...state, count: 5 })spread is not needed. An array passed to an array store is diffed in place. - Update by mutating the draft.
setState((draft) => { draft.items.push(item); })replaces the immutable copy with spread andmap. Only the touched properties notify. For server data,setState(reconcile(next, { key: "id" }))updates by key. - There is no lazy initializer. The body runs once, so
useStore(expensive())computesexpensive()once.
The types enforce the choice. A Store is readonly, so assigning to it is an error, and a union with null is
refused by useStore:
import { useStore } from "@shadow-js/core";
interface Todo {
id: number;
}
export function Types() {
const [state] = useStore({ count: 0 });
// @ts-expect-error a store is readonly; write through its setter
state.count = 1;
const [list] = useStore<Todo[] | null>(null);
return <p>{list()?.length ?? 0}</p>;
}