Skip to content

Composition Over Stores

SolidJS has createStore. Vue has reactive(). These provide “deep reactivity”—you can mutate nested properties and the framework tracks the changes automatically.

Rimitive doesn’t have stores. This is intentional.


Deep reactivity requires proxies. Proxies have costs:

  • Magic behavior: Property access triggers invisible tracking. Mutations trigger invisible updates. Hard to debug.
  • Performance overhead: Every property access goes through the proxy trap.
  • Complexity: Edge cases around arrays, Maps, Sets, class instances, etc.
  • Encourages sprawl: Big nested state objects instead of focused, composable units.

There’s nothing saying you can’t create your own primitive—it’s just that rimitive doesn’t ship one by default.


Instead of one big store, compose focused behaviors:

// Instead of this:
const store = createStore({
user: {
profile: { name: '', avatar: '' },
settings: { theme: 'light', notifications: true }
},
cart: {
items: [],
total: 0
}
});
// Do this:
const userProfile = (svc: SignalsSvc) => () => {
const name = svc.signal('');
const avatar = svc.signal('');
return {
name,
avatar,
setName: (n: string) => name(n),
setAvatar: (a: string) => avatar(a),
};
};
const userSettings = (svc: SignalsSvc) => () => {
const theme = svc.signal<'light' | 'dark'>('light');
const notifications = svc.signal(true);
return {
theme,
notifications,
toggleTheme: () => theme(theme() === 'light' ? 'dark' : 'light'),
};
};
const cart = (svc: SignalsSvc) => () => {
const items = svc.signal<CartItem[]>([]);
const total = svc.computed(() =>
items().reduce((sum, item) => sum + item.price * item.qty, 0)
);
return {
items,
total,
addItem: (item: CartItem) => items([...items(), item]),
removeItem: (id: string) => items(items().filter(i => i.id !== id)),
};
};

Each behavior:

  • Has a clear, focused purpose
  • Encapsulates its own logic
  • Is independently testable
  • Can be composed with others
  • Is portable

Behaviors can depend on other behaviors by calling them directly:

const checkout = (svc: SignalsSvc) => () => {
const { computed } = svc;
// Call other behaviors directly with the service
const c = cart(svc)();
const profile = userProfile(svc)();
const canCheckout = computed(() =>
c.items().length > 0 && profile.name() !== ''
);
const submit = async () => {
if (!canCheckout()) return;
await submitOrder({
items: c.items(),
customer: profile.name(),
});
};
return { canCheckout, submit };
};

For reactive arrays, use a signal holding an array:

const todoList = ({ signal }: SignalsSvc) => () => {
const items = signal<Todo[]>([]);
return {
items,
add: (text: string) => {
items([...items(), { id: crypto.randomUUID(), text, done: false }]);
},
toggle: (id: string) => {
items(items().map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
},
remove: (id: string) => {
items(items().filter(t => t.id !== id));
},
};
};

This follows an immutable pattern which is:

  • Explicit (you see the update happening)
  • Debuggable (you can log the before/after)
  • Predictable (no proxy magic)

If you genuinely need to update nested data, create a focused updater:

const nestedSettings = (svc: SignalsSvc) => () => {
const settings = svc.signal({
display: { theme: 'light', fontSize: 14 },
privacy: { shareData: false, analytics: true },
});
// Focused updaters for specific paths
const setTheme = (theme: string) => {
settings({
...settings(),
display: { ...settings().display, theme }
});
};
const setFontSize = (size: number) => {
settings({
...settings(),
display: { ...settings().display, fontSize: size }
});
};
// Or a generic path updater if you really need it
const update = <K extends keyof Settings>(
section: K,
updates: Partial<Settings[K]>
) => {
settings({
...settings(),
[section]: { ...settings()[section], ...updates }
});
};
return { settings, setTheme, setFontSize, update };
};

If you’re convinced you need proxy-based stores, you can build them on top of Rimitive’s primitives. The signals are there—wrap them in proxies if you want.

Or use an external store library and connect it via effects:

import { createStore } from 'some-store-library';
const externalStore = createStore({ count: 0 });
const bridged = (svc: SignalsSvc) => () => {
const count = svc.signal(externalStore.getState().count);
// Sync from external store to signal
externalStore.subscribe((state) => {
count(state.count);
});
return { count };
};

But consider: if you’re reaching for proxies, you might be fighting the framework. Rimitive’s composition model works best when you embrace it.