ilokesto

Utility

@ilokesto/state/utils exports the pipe helpers that are not tied to a framework adapter. The separate @ilokesto/state/adaptor subpath exports the Immer-backed update helper:

  • pipe creates an @ilokesto/store instance and applies registered middleware in order.
  • definePipeableMiddleware registers custom middleware metadata for pipe.use(...).
  • PipeConfigurationError reports invalid runtime pipe configurations.
  • adaptor wraps Immer produce so object updates can be written with draft mutation syntax.

Use Utility pages when you are building the store before connecting it to React, Vue, Svelte, Solid, or Angular.

Why utilities are separate from adapters

Framework adapters answer “how does this state become reactive in my UI?” Utilities answer “how do I prepare or write the state itself?” That separation lets the same store setup be reused across multiple frameworks or outside UI code.

import { create } from '@ilokesto/state/react';
import { jsonStorage, logger, persist } from '@ilokesto/state/middleware';
import { pipe } from '@ilokesto/state/utils';

type PreferencesState = { theme: 'system' | 'light' | 'dark' };

const decodePreferences = (value: unknown): PreferencesState | null => {
  if (typeof value !== 'object' || value === null || !('theme' in value)) return null;
  return value.theme === 'system' || value.theme === 'light' || value.theme === 'dark'
    ? { theme: value.theme }
    : null;
};

const preferencesStore = pipe
  .use(persist({ key: 'preferences', storage: jsonStorage(() => window.localStorage), decode: decodePreferences }))
  .use(logger({ collapsed: true }))
  .create<PreferencesState>({ theme: 'system' });

export const usePreferences = create(preferencesStore);

export async function restorePreferences() {
  await preferencesStore.persist.rehydrate();
}

The component still uses the normal adapter API. Call restorePreferences() from client startup or an effect, handle errors, and enable editing after restoration. The utility layer only prepares the store passed into create; creating it performs no storage I/O.

Installation notes

pipe only needs @ilokesto/state and its dependency on @ilokesto/store.

adaptor uses immer, which is an optional peer dependency. Install it only if you use adaptor.

pnpm add immer

Choose the right utility

UtilityUse it whenAvoid it when
pipeYou want to create a store and apply registered middleware left-to-right.You already own a Store<T> and want to mutate that exact instance manually.
definePipeableMiddlewareYou are exposing custom middleware to a pipe chain.The middleware is only used outside pipe.
adaptorObject updates are easier to express as draft mutations.State is primitive, or you do not want to install immer.

Typical composition flow

  1. Define initial state.
  2. Compose middleware with pipe if the state needs persistence, logging, validation, debouncing, or DevTools.
  3. Pass the resulting store to a framework adapter’s create.
  4. Use adaptor inside setState calls only where draft syntax improves readability.
import { create } from '@ilokesto/state/react';
import { validate } from '@ilokesto/state/middleware';
import { adaptor } from '@ilokesto/state/adaptor';
import { pipe } from '@ilokesto/state/utils';

const profileStore = pipe
  .use(validate(profileSchema))
  .create({ name: '', tags: [] as string[] });

const useProfile = create(profileStore);

function AddTagButton({ tag }: { tag: string }) {
  const [, setProfile] = useProfile((state) => state.tags);

  return (
    <button
      onClick={() =>
        setProfile(adaptor((draft) => {
          draft.tags.push(tag);
        }))
      }
    >
      Add tag
    </button>
  );
}

On this page