[]previous and next page

useState becomes useStore

useState becomes useStore in the React to ShadowJS guide.

For agentsUpdated

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.

src/profile.tsxtsx
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 }) changes count and keeps the other keys; React's setState({ ...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 and map. 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()) computes expensive() 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:

state-types.tsxtsx
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>;
}