Utilities

Store (@reckona/mreact-store)

@reckona/mreact-store provides global and shared reactive state primitives for Mreact. Use it for application-wide UI state or domain state that should be updated through explicit actions, selectors, subscriptions, transactions, or persistence.

Source: packages/store on GitHub.

Install

pnpm add @reckona/mreact-store

When to use it

Use @reckona/mreact-store for state owned by your app: selected workspace, open panels, draft UI state, local preferences, optimistic client state, or domain state that multiple components need to read.

Use @reckona/mreact-query for server state: data fetched from APIs, request dedupe, cache invalidation, hydration, and mutations. Use form-specific helpers for form state and validation.

Basic store

import { createStore, shallowEqual } from "@reckona/mreact-store";

const counter = createStore({ count: 0, label: "counter" });

const count = counter.select((state) => state.count);
const labelSnapshot = counter.select((state) => ({ label: state.label }), shallowEqual);

counter.set((state) => ({ count: state.count + 1 }));

store.select() returns a selected cell. Use dispose() for selectors created outside a framework cleanup lifecycle.

Actions and transactions

Keep write operations behind small functions so callers do not repeat update logic. Use transaction() when several updates should notify subscribers once.

const workspace = createStore({
  activeProjectId: "docs",
  sidebarOpen: true,
  selectedIds: [] as string[],
});

export function selectProject(projectId: string) {
  workspace.transaction(() => {
    workspace.set({ activeProjectId: projectId });
    workspace.set({ selectedIds: [] });
  });
}

Selectors only notify when their selected value changes. Use shallowEqual for object-shaped selector results that should not notify when the object fields are equal.

Request-isolated stores

Never put per-request server state in a module-level browser-style singleton. Use createRequestStoreFactory() when server code needs a fresh store per request.

import { createRequestStoreFactory } from "@reckona/mreact-store";

const createRequestStore = createRequestStoreFactory(() => ({
  userId: undefined as string | undefined,
  flash: undefined as string | undefined,
}));

export function requestStore() {
  return createRequestStore();
}

This keeps request-isolated state separate from process-level state in long-lived Node, container, Cloudflare, or Lambda runtimes.

Persistence

The persist option connects store state to a storage adapter. Use it for non-secret browser preferences such as theme, density, or last-opened panel. Do not persist secrets, access tokens, authorization data, or server-only state in browser storage.

For simple write-only adapters, pass a callback. The callback runs after each committed change and receives the next store state.

const preferences = createStore(
  { density: "comfortable", theme: "system" },
  {
    persist(state) {
      localStorage.setItem("preferences", JSON.stringify(state));
    },
  },
);

Use the descriptor form when the store should hydrate from storage, migrate older versions, or serialize asynchronous writes in commit order. load() runs after store creation, and a hydrated state does not immediately save itself back to storage. When the saved version differs from the configured version, migrate(state, savedVersion) can normalize the loaded state before it replaces the current value.

interface Preferences {
  density: "compact" | "comfortable";
  theme: "dark" | "light" | "system";
}

const preferences = createStore<Preferences>(
  { density: "comfortable", theme: "system" },
  {
    persist: {
      load() {
        const text = localStorage.getItem("preferences");
        return text === null
          ? undefined
          : JSON.parse(text) as { state: Preferences; version: number };
      },
      migrate(state, savedVersion) {
        return savedVersion === 1
          ? { ...state, density: state.density ?? "comfortable" }
          : state;
      },
      save(state) {
        localStorage.setItem(
          "preferences",
          JSON.stringify({ state, version: 2 }),
        );
      },
      version: 2,
    },
  },
);

Outside the framework runtime

Use store.subscribe() for integration code that lives outside a component lifecycle, such as devtools, adapters, or imperative widgets. Dispose the subscription when the integration shuts down.

API surface

  • createStore()
  • store.get()
  • store.set()
  • store.replace()
  • store.select()
  • store.subscribe()
  • store.transaction()
  • createRequestStoreFactory()
  • shallowEqual()

API reference: these links open the generated TypeDoc pages inside this docs site.