[]previous and next page

Values that can be null or change type

Values that can be null or change type in the State with useStore guide.

For agentsUpdated

useStore is the one state hook. A value whose type is a union (User | null, Todo[] | undefined, string | { n: number }) is a single value too: you replace it as a whole with the setter, and plain data you assign to it is read through the same fine-grained proxies a store uses, so user()?.name tracks exactly what it reads.

The initial value decides the shape. null, a primitive, a function, a Date or a Map starts a single cell. A plain object or array starts a deep store even when the type includes null: useStore<User | null>({ name: "Ada" }) gives you user.name, not user(). So start a nullable value at null, as Session does, or keep the nullable part inside a store, as { user: User | null } does in the control-flow guide.

src/session.tsxtsx
import { useStore } from "@shadow-js/core";

interface User {
  name: string;
}

export function Session() {
  const [user, setUser] = useStore<User | null>(null);
  const label = user()?.name ?? "signed out";

  return (
    <button type="button" onClick={() => setUser((current: User | null) => (current === null ? { name: "Ada" } : null))}>
      {label}
    </button>
  );
}
src/union.tsts
import { useStore } from "@shadow-js/core";

interface Todo {
  id: number;
}

const [todos, setTodos] = useStore<Todo[] | null>(null);
const length: number | undefined = todos()?.length;
setTodos([{ id: 1 }]);
setTodos(null);
void length;

Setter details:

  • The updater runs untracked. To store a function, return it from an updater: set(() => fn).
  • For a value typed Something | null, TypeScript cannot infer the updater's parameter from the setter's overloads and reports it as implicitly any. Annotate it, as Session above does: setUser((current: User | null) => ...).