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:
pipecreates an@ilokesto/storeinstance and applies registered middleware in order.definePipeableMiddlewareregisters custom middleware metadata forpipe.use(...).PipeConfigurationErrorreports invalid runtime pipe configurations.adaptorwraps Immerproduceso 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 immerChoose the right utility
| Utility | Use it when | Avoid it when |
|---|---|---|
pipe | You 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. |
definePipeableMiddleware | You are exposing custom middleware to a pipe chain. | The middleware is only used outside pipe. |
adaptor | Object updates are easier to express as draft mutations. | State is primitive, or you do not want to install immer. |
Typical composition flow
- Define initial state.
- Compose middleware with
pipeif the state needs persistence, logging, validation, debouncing, or DevTools. - Pass the resulting store to a framework adapter’s
create. - Use
adaptorinsidesetStatecalls 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>
);
}Related pages
- Middleware explains the middleware functions that
pipecommonly composes. - Plain state guide shows when
setState-style updates are enough. - Reducer state guide shows when named actions are a better fit.