ilokesto

Patterns

Practical ways to organize stores, actions, and derived reads.

Patterns

The API is small, but the way you organize it matters a lot. Most real projects do not struggle with create() itself. They struggle with where actions live, how many stores to create, and when to keep logic inside components versus next to the store.

Keep store definition and actions close, but not identical

A common pattern is to keep the hook in one module and export actions from a sibling module. That keeps create() small while still letting you build a real state API.

// stores/counter.ts
export const useCounter = create({ count: 0 });

// actions/counter.ts
import { useCounter } from '../stores/counter';

export const increment = () => {
  useCounter.writeOnly()(state => ({ count: state.count + 1 }));
};

This pattern works especially well when several components need to trigger the same write path.

Use selectors for cheap derived reads

The first choice for derived state should usually be a selector, not another store field.

const useStore = create({ items: [] });

// Simple derived state
const useTotalItems = () => useStore(state => state.items.length);

// Expensive calculation kept behind one selector
const selectExpensiveData = (state) => {
  // calculate something heavy...
};
const useExpensiveData = () => useStore(selectExpensiveData);

If a calculation is genuinely expensive, make the selector reusable and keep the component unaware of the implementation detail.

Prefer several focused stores over one giant store

@ilokesto/state does not force a single global state tree. In many apps, smaller focused stores are easier to evolve than one oversized store with unrelated concerns.

// auth.ts
export const useAuth = create({ user: null });

// products.ts
export const useProducts = create({ list: [] });

The split becomes worth it when domains change at different rates or belong to different screens.

Let effects sit around actions, not inside the store shape

Because writeOnly() is available outside components, you can keep effectful workflows near the action that causes them instead of mixing effect state into the store definition itself.

// Action with side effect
export const fetchUser = async (id: string) => {
  const user = await api.getUser(id);
  useAuth.writeOnly()({ user });
};

That usually produces cleaner boundaries than storing “loading logic” directly in the store factory.

A simple rule of thumb

  • Use one store when the state truly changes together.
  • Split stores when domains are independent.
  • Use selectors before adding more derived fields.
  • Move writes behind actions when the same update appears in more than one component.

On this page