ilokesto

Core Concepts

The state, update, and subscription contract behind Store.

Core Concepts

@ilokesto/store is a synchronous value container. It holds one state value, replaces that value, and notifies listeners after each successful replacement.

Store as a contract

Store<T> defines the contract for a single state atom. The public API stays small: create a store, read its current or initial value, replace the value, add middleware, and subscribe to change notifications.

The contract excludes framework rendering, selectors, reducers, persistence, data fetching, devtools, and async batching. Build those concerns around a store when a project needs them.

State snapshot

getState() returns the current state reference. getInitialState() returns the value that was passed to the constructor.

Both methods return Readonly<T>. Treat the value as a TypeScript snapshot. Store does not freeze, clone, or proxy the state at runtime.

const store = new Store({ count: 0 });

const current = store.getState();
const initial = store.getInitialState();

Reading

Reading is direct and synchronous. There is no derived read system and no property level tracking.

Calculate derived values outside the store or in a higher level adapter:

const count = store.getState().count;
const isEmpty = count === 0;

Updating

setState accepts either the next value or an updater function.

store.setState({ count: 1 });

store.setState((prev) => ({
  ...prev,
  count: prev.count + 1,
}));

Use the updater form when the next state depends on the previous state.

Replacement semantics

Updates replace the whole state value. Object state is not merged.

const store = new Store({ a: 1, b: 2 });

store.setState({ a: 10 });
// Current state is { a: 10 }. The b field was not preserved.

Preserve fields yourself when you want to keep them:

store.setState((prev) => ({ ...prev, a: 10 }));

Subscription lifecycle

subscribe registers a listener and returns an unsubscribe function.

const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});

unsubscribe();

Listeners run synchronously after a successful replacement. The listener set is snapshotted for notification, so removing a listener during notification does not mutate the active iteration array.

Store keeps listener references in a Set. Registering the same function reference more than once still stores one listener.

Equality and bailout behavior

Before replacing state, Store compares the previous and resolved next value with Object.is.

If Object.is(prevState, nextState) is true, Store returns without replacing state and without notifying listeners.

store.setState((prev) => {
  if (prev.count >= 10) return prev;

  return { ...prev, count: prev.count + 1 };
});

For objects and arrays, represent a change with a new reference.

Mutation caveats

Do not mutate the value returned by getState().

const state = store.getState();

state.items.push("new item");

That direct mutation does not call setState, so listeners are not notified. It also makes later Object.is checks harder to reason about because the same object reference has already changed.

Use an immutable replacement instead:

store.setState((prev) => ({
  ...prev,
  items: [...prev.items, "new item"],
}));

Framework adapter relationship

Framework adapters treat Store as the state and subscription primitive. The adapter owns framework specific concerns: rendering, lifecycle cleanup, selectors, scheduling, and memoization rules.

That separation keeps @ilokesto/store usable in plain TypeScript and gives framework packages a stable base to build on.

On this page