createStore
Creates a deeply-reactive store backed by a Proxy. Reads track each property accessed; only the parts that change trigger updates.
Store properties hold plain values, not accessors. The proxy
already tracks reads per-property — wrapping a value in
() => state.foo produces a getter that won't track when called,
which looks like a reactivity bug but is a category error. If
you have a signal-shaped piece of state, make it a property of the
store ({ foo: 1 }) rather than nesting an accessor inside
({ foo: () => signal() }).
The setter takes a draft-mutating function — mutate the draft
in place (canonical). The callback may also return a new value:
arrays are replaced by index (length adjusted), objects are
shallow-diffed at the top level (keys present in the returned value
are written, missing keys deleted). Use the return form for shapes
where mutation is awkward — most commonly removing items via
filter. The setter does not do keyed reconciliation; for
that, use the derived/projection form (or createProjection).
- Plain form —
createStore(initialValue, options?): wraps a value in a reactive proxy. - Derived form —
createStore(fn, seed, options?): a projection store whose contents are computed byfn(draft).fnmay be sync, async, or anAsyncIterable; the projection's result reconciles against the existing store byoptions.key(default"id") for stable identity.
Import
import { createStore } from "solid-js";Type signature
function createStore<T extends object = {}>( initialValue: T | Store<T>, options?: StoreOptions): [get: Store<T>, set: StoreSetter<T>];function createStore<T extends object = {}>( fn: () => T | Promise<T> | AsyncIterable<T>, seed?: null, options?: Omit<ProjectionOptions, "seedLoadingValue">): [get: Store<T>, set: StoreSetter<T>];function createStore<T extends object = {}>( fn: (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>, seed: T | Store<T>, options?: ProjectionOptions): [get: Store<T>, set: StoreSetter<T>];Parameters
initialValue
- Type:
T | Store<T>
Plain object or array to wrap, or an existing store to share.
options
- Type:
StoreOptions | ProjectionOptions - Optional
name and shallow in the plain form; the derived form also accepts key, seedLoadingValue, deferStream, and ssrSource.
fn
- Type:
(() => T | Promise<T> | AsyncIterable<T>) | (draft: T) => void | T | Promise<void | T> | AsyncIterable<void | T>
Derive function for the projection form. Receives a draft and either mutates it or returns a new value; may be sync, async, or an AsyncIterable.
seed
- Type:
null | T | Store<T> - Optional
Initial contents of the projection store and the draft the first derive receives. With an async fn, readers see the seed before the first result only when seedLoadingValue is set; otherwise they suspend.
Return value
[store: Store<T>, setStore: StoreSetter<T>]
A tuple. The store is a read-only proxy that tracks each property you read; the setter receives a draft to mutate, and the changes commit when it returns.
Examples
const [state, setState] = createStore({ user: { name: "Ada", age: 36 }, todos: [] as { id: string; text: string; done: boolean }[],});
// Canonical: mutate the draft in place.setState((s) => { s.user.age = 37;});setState((s) => { s.todos.push({ id: "1", text: "x", done: false });});
// Return form: reach for it when mutation is awkward.setState((s) => s.todos.filter((t) => !t.done)); // remove itemssetState((s) => ({ ...s, user: { name: "Grace", age: 85 } })); // shallow replace// Derived store — auto-fetches & reconciles by `id`.const [users] = createStore( async () => fetch("/users").then((r) => r.json()), [] as User[]);Hydration: the derived form accepts ProjectionOptions, including
an ssrSource field
("server" | "hybrid" | "client"). See the ssrSource option.
Caveats
- Store properties hold plain values. Nesting an accessor inside a store (
{ count: () => count() }) does not track when read. - Mutating the store outside the setter (
state.items.push(x)) is not reactive. Write through the setter's draft. - The setter does not reconcile by key. Replacing an array replaces rows by index; use
reconcile()or the derived form to keep row identity. - Plain objects, arrays, and class instances are wrapped. Built-in objects such as
Map,Date, and frozen objects are stored raw; the property that holds them still tracks reassignment.
Common problems
Learn more
Related types
ProjectionOptions
Options for derived/projected stores created with
createStore(fn, seed, options?), createProjection(fn, seed, options?),
or createOptimisticStore(fn, seed, options?).
interface ProjectionOptions extends StoreOptions { key?: string | ((item: NonNullable<any>) => any) | null; seedLoadingValue?: boolean; deferStream?: boolean; ssrSource?: "server" | "hybrid" | "client";}key
- Type:
string | ((item: NonNullable<any>) => any) | null
Key property name or function for reconciliation identity; null merges positionally
seedLoadingValue
- Type:
boolean
Treat the seed as the first committed value: the store is born committed with the seed's
contents, shown until the derive's first real answer lands. While that
first answer is in flight, reads serve the seed everywhere — nothing
suspends to a <Loading> boundary, no update is held, and
isPending stays false (the seed answers by declaration; first-load
affordances belong to the data, e.g. a skeleton: true field in the
seed). Once the first answer lands (reconciled into the seed), refetches
use normal pending semantics with isPending true.
The store equivalent of MemoOptions.loadingValue; the seed already
carries the placeholder shape, so this option is the only opt-in needed.
deferStream
- Type:
boolean
Defer the SSR stream flush until this primitive's first value is
resolved. Lets late-resolving sources hold the document open
rather than forcing the surrounding <Loading> boundary to render
its fallback into the HTML. Server-only; ignored on the client.
ssrSource
- Type:
"server" | "hybrid" | "client"
Hydration policy. Decides what initial value the client uses and whether the compute re-runs.
"server"(default): client uses the serialized server value as initial state. Compute does not re-run for the initial value — the serialized result is authoritative. Choose this when the compute is deterministic from server-available inputs."hybrid": client uses the serialized server value first; then, for a compute that returns an async iterable, the client continues the stream from it (the server consumed exactly one yield; the client's first yield duplicates it and is discarded, later yields update the node). For a sync or promise-shaped compute,"hybrid"is identical to"server": the serialized value is adopted and the compute does not re-run until a dependency changes orrefresh(). Choose this for streaming sources the client should keep consuming after hydration."client": skip the server value entirely. Compute is deferred until hydration completes, then runs as if first-mounted. Choose this for client-only state where serialization is meaningless. Two forms decide what the pre-compute window renders: bare (structural) — the source suspends on the server as a final hole and the nearest<Loading>boundary renders its fallback, handing the position to the client; or a declared first value —loadingValueon signal-family sources (loadingValue: undefinedis a valid declaration),seedLoadingValue: trueon store-family sources — which renders provisional data with no boundary involvement.
ProjectionStoreReturn
Tuple returned by the derived createStore(fn, seed, options?) form.
type ProjectionStoreReturn<T> = [get: Store<T>, set: StoreSetter<T>];StoreOptions
Options shared by all store primitives.
interface StoreOptions { name?: string; shallow?: boolean;}name
- Type:
string
Debug name (dev and observe builds). Property nodes are labelled
<name>.<key> in attribution output (todos.title); a derived store's
projection node carries the name itself.
shallow
- Type:
boolean
Single-layer store: root keys reactive, values raw records replaced by reference
StoreReturn
Tuple returned by the plain createStore(initialValue, options?) form.
type StoreReturn<T> = [get: Store<T>, set: StoreSetter<T>];StoreSetter
A store setter. The callback receives a writable draft of the store.
- Mutate in place (canonical):
s.foo = 1,s.list.push(x),s.list.splice(i, 1). This is the default form for most updates. - Return a new value: for shapes where mutation is awkward, most
commonly removing items (
s => s.list.filter(...)). Arrays are replaced by index (length adjusted); objects are shallow-diffed at the top level (keys present in the returned value are written, missing keys deleted).
The setter does not perform keyed reconciliation. If you need surviving
items to keep their store identity across full-array replacement, use the
projection form — createStore(fn, seed, { key }) or
createProjection(fn, seed, { key }) — whose derive function reconciles
its return by options.key.
type StoreSetter<T> = (fn: (state: T) => T | void) => void;