~ Sinux ~
Signal-driven state management for React

Sinux replaces free-form state mutators with named, async, composable signals.
Every action is traceable. Every mutation has a name.
Documentation • Getting Started • Examples
Why Sinux?
| Sinux | Zustand |
|---|
| Actions | Named signals on the store | Anonymous set() calls anywhere |
| Async | First-class — signals return Promises | Convention-based, DIY |
| Pipeline | Stack handlers on a signal with .add() | No equivalent |
| Server state | Built-in TanStack Query + Apollo adapters | Separate wiring |
| Selectors | useSyncExternalStore with selector | Same |
Quick Start
npm install @sinuxjs/core @sinuxjs/react
import { createStore } from '@sinuxjs/core';
import { useStore } from '@sinuxjs/react';
// Create a store with named signals
const counterStore = createStore(
{ count: 0 },
{
increment: (state) => ({ count: state.count + 1 }),
add: (state, amount: number) => ({ count: state.count + amount }),
}
);
// Use in React
function Counter() {
const { count } = useStore(counterStore, (s) => ({ count: s.count }));
return (
<div>
<span>{count}</span>
<button onClick={() => counterStore.increment()}>+1</button>
<button onClick={() => counterStore.add(10)}>+10</button>
</div>
);
}
Signals
Signals are named async commands. They receive state, return partial updates, and compose naturally.
const store = createStore({ items: [] }, {
// Sync
addItem: (state, item) => ({ items: [...state.items, item] }),
// Async
loadItems: async (state) => {
const items = await fetch('/api/items').then(r => r.json());
return { items };
},
});
await store.addItem('hello');
await store.loadItems();
// Stack handlers on the same signal (command pipeline)
store.addItem.add((state, item) => {
analytics.track('item_added', { item });
});
Middleware
Third argument to createStore. Three hooks: onInit, onDispatch, onStateChange.
import { createStore, persist, devtools, immer } from '@sinuxjs/core';
const store = createStore(
{ todos: [] },
{
addTodo: (state, text) => {
state.todos.push({ text, done: false }); // immer draft
},
toggle: (state, index) => {
state.todos[index].done = !state.todos[index].done;
},
},
[
persist({ key: 'todos' }), // localStorage persistence
devtools({ name: 'Todo Store' }), // Redux DevTools
immer(), // mutable-style updates
]
);
Custom middleware
const logger = {
onStateChange(state, prevState, signalName) {
console.log(`[${signalName}]`, prevState, '→', state);
},
};
const validator = {
onDispatch({ signalName, args, getState, next }) {
if (signalName === 'setEmail' && !args[0]?.includes('@')) {
return getState(); // reject
}
return next(getState(), ...args);
},
};
Computed State
import { computed } from '@sinuxjs/core';
import { useComputed } from '@sinuxjs/react';
const activeTodos = computed(todoStore, (s) =>
s.todos.filter((t) => !t.done)
);
function ActiveCount() {
const todos = useComputed(activeTodos);
return <span>{todos.length} remaining</span>;
}
Multi-Store
Signals can trigger other stores. Generators orchestrate sequential flows.
authStore.login.add(async (state, email, password) => {
await cartStore.load();
await notificationStore.add('Welcome back!');
});
// Or with generators
authStore.login.add((state, email, password) => {
return function* () {
yield cartStore.load();
yield notificationStore.add('Welcome back!');
}();
});
Subscribe to multiple stores in React:
import { combine, useStores } from '@sinuxjs/react';
const app = combine(authStore, cartStore);
const { user, items } = useStores(app, (s) => ({ user: s.user, items: s.items }));
Server State
TanStack Query
npm install @sinuxjs/tanstack-query @tanstack/react-query