ilokesto

Organizing Stores

Choose module-level stores, factories, or smaller stores by ownership.

Organizing Stores

Store organization starts with one question: who owns this state?

If the whole app shares it, a module-level store is simple. If each screen, tab, request, or test needs its own copy, use a factory. If unrelated fields change for unrelated reasons, split them into separate stores.

Module-level store

A module-level store works well for state shared by many parts of an app. Keep the state shape focused, then export small functions that describe the allowed updates.

import { Store } from "@ilokesto/store";

type AuthState = {
  user: { id: string; name: string } | null;
  status: "anonymous" | "signed-in";
};

const initialAuthState: AuthState = {
  user: null,
  status: "anonymous",
};

export const authStore = new Store<AuthState>(initialAuthState);

export function signIn(user: { id: string; name: string }) {
  authStore.setState({ user, status: "signed-in" });
}

export function signOut() {
  authStore.setState(initialAuthState);
}

Use this pattern for app-wide state such as auth, theme, feature flags, or a small current-workspace value.

Store factory

A factory creates a fresh store for each feature instance. This is useful when state must not leak across users, screens, tabs, tests, or server request scopes.

import { Store } from "@ilokesto/store";

type CounterState = {
  count: number;
};

export function createCounterStore(initialCount = 0) {
  const store = new Store<CounterState>({ count: initialCount });

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

Prefer a factory when a module-level store would make tests order-dependent or accidentally share state between app instances.

Splitting stores

Split stores by responsibility when unrelated consumers should not share the same notification stream.

import { Store } from "@ilokesto/store";

export const themeStore = new Store<"light" | "dark">("light");

export const sessionStore = new Store({
  userId: null as string | null,
  activeWorkspaceId: null as string | null,
  tokenExpiresAt: null as number | null,
});

export const draftStore = new Store({
  title: "",
  body: "",
});

Separate stores make ownership clearer. They also make resets and tests smaller because each store has one job.

Avoiding one giant store

A single app-wide object can look convenient, but it often becomes harder to change safely.

Watch for these signals:

  • unrelated updates require spreading many fields,
  • tests need large fixtures for tiny behaviors,
  • subscribers wake up for changes they do not care about,
  • reset logic has to know about every feature.

Use a store per domain instead. If two domains need to coordinate, write an app-level function that updates each store explicitly.

export function clearWorkspace() {
  sessionStore.setState((prev) => ({ ...prev, activeWorkspaceId: null }));
  draftStore.setState({ title: "", body: "" });
}

This keeps coordination visible without turning every feature into one shared state object.

Next

  • Continue with Updating and Testing for reset helpers, derived values, async flows, and isolated tests.
  • Read Core Concepts if you want the deeper model behind replacement updates and subscriptions.

On this page