Values that can be null or change type
Values that can be null or change type in the State with useStore guide.
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.
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>
);
}
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 implicitlyany. Annotate it, asSessionabove does:setUser((current: User | null) => ...).