A powerful and hopefully simple state management library for React.
Docs can be found here: https://duriantaco.github.io/neutrix
- Overview
- Installation
- Motivation
- Quick Decision Guide
- Usage
- API Reference
- Examples
- Advanced Topics
- FAQ
- Contributing
- License
Whether you’re building a small side project or a large-scale application, Neutrix’s flexible API helps you scale your state management without sacrificing developer experience.
npm install neutrix
yarn add neutrix
In React apps, global state vs. feature-specific state is a constant tension. Libraries often force one pattern:
Zustand-like: No providers, direct store usage (great for global, but not for isolation). Redux-like: Strictly uses Providers (great for large apps, but can feel overkill for small ones). Developers either wrap everything in Providers or never use them at all—both extremes can be suboptimal.
Neutrix unifies both patterns into one library:
- Simple, Hook-Only usage (like Zustand) with no Provider.
- Provider usage (like Redux) for SSR, multiple stores, or advanced setups.
Choose whichever approach suits your current app scale—no separate libraries or big rewrites needed later.
-
Use “no Provider” if you have a single store and want minimal setup. Perfect for small/medium apps.
-
Use “with Provider” if you need multiple stores, SSR, or prefer the Redux-like pattern with DevTools scope.
If you want hook-only usage, you simply ignore the returned :
// Hook-only usage
export const { useStore, store } = createNeutrixStore({ count: 0 });
function Counter() {
const count = useStore(s => s.count);
// ...
}
If you want provider-based usage, you do:
export const { store, useStore, Provider } = createNeutrixStore({ count: 0 });
function App() {
return (
<Provider>
<Counter />
</Provider>
);
}
The simplest approach: createNeutrixStore without a provider. No context, no overhead:
import { createNeutrixStore } from 'neutrix';
const { useStore } = createNeutrixStore({ count: 0 });
function Counter() {
const count = useStore(s => s.count);
return (
<button onClick={() => useStore.store.set('count', count + 1)}>
Count: {count}
</button>
);
}
- Read state: useStore(s => s.something)
- Write state: useStore.store.set('something', newValue) Done. No needed.
This is effectively the same as the Quick Start example, but more explicit. You can pass { provider: false
} or omit it entirely:
import React from 'react';
import { createNeutrixStore } from 'neutrix';
interface AppState {
user: null | { name: string };
theme: 'light' | 'dark';
}
// returns a "hook-only" store by default
const { useStore, store } = createNeutrixStore<AppState>(
{ user: null, theme: 'light' },
{
name: 'appStore',
// provider: false, // optional
}
);
function Profile() {
const user = useStore(s => s.user);
return <div>{user?.name ?? 'Guest'}</div>;
}
function ThemeSwitcher() {
const theme = useStore(s => s.theme);
const toggleTheme = () =>
store.set('theme', theme === 'light' ? 'dark' : 'light');
return <button onClick={toggleTheme}>Current theme: {theme}</button>;
}
export default function App() {
return (
<div>
<Profile />
<ThemeSwitcher />
</div>
);
}
No , no context overhead, just a single store hook. Great for simpler apps.
If you prefer a Provider pattern or need SSR, you can set provider: true
:
import React from 'react';
import { createNeutrixStore } from 'neutrix';
interface AppState {
user: null | { name: string };
theme: 'light' | 'dark';
}
const { store, useStore, Provider } = createNeutrixStore<AppState>(
{ user: null, theme: 'light' },
{
provider: true, // <--- This creates a context-based store
devTools: true
}
);
function Profile() {
const user = useStore(s => s.user);
return <div>{user?.name ?? 'Guest'}</div>;
}
function ThemeSwitcher() {
const theme = useStore(s => s.theme);
const toggleTheme = () =>
store.set('theme', theme === 'light' ? 'dark' : 'light');
return <button onClick={toggleTheme}>Current theme: {theme}</button>;
}
export default function App() {
return (
<Provider>
<Profile />
<ThemeSwitcher />
</Provider>
);
}
Now, useStore reads from the context-based store. Perfect for larger apps, SSR, or when you need multiple store instances.
You can create multiple store instances (e.g. userStore, cartStore) and combine them under a single if you want:
import React from 'react';
import { NeutrixProvider, createNeutrixStore } from 'neutrix';
interface UserState {
user: null | { name: string };
}
interface CartState {
items: string[];
}
const { store: userStore, useStore: useUserStore } =
createNeutrixStore<UserState>({ user: null }, { provider: true });
const { store: cartStore, useStore: useCartStore } =
createNeutrixStore<CartState>({ items: [] }, { provider: true });
function App() {
return (
<NeutrixProvider stores={{ userStore, cartStore }}>
<Profile />
<Cart />
</NeutrixProvider>
);
}
function Profile() {
const user = useUserStore(s => s.user);
return <div>{user?.name ?? 'Guest'}</div>;
}
function Cart() {
const items = useCartStore(s => s.items);
return <div>Cart has {items.length} items</div>;
}
function createNeutrixStore<T extends State>(
initialState: T,
options?: StoreOptions & { provider?: boolean }
):
| StoreHook<T> // if provider=false or omitted
| { store: Store<T>, useStore: Function, Provider: React.FC } // if provider=true
Creates either a hook-based store (if provider: false
or omitted) or a provider-based store (if provider: true
). This is the recommended function for most users.
-
Parameters:
-
initialState: Your initial state object
-
options: StoreOptions & { provider?: boolean }
provider
: Iftrue
, returns{ store, useStore, Provider }
- If
false
or omitted, returns a single hook plus.store
function createCoreStore<T extends State>(
initialState: T,
options?: StoreOptions
): Store<T>;
Low-level store creation, without any React hooks. Perfect if you need to do SSR manually, or integrate with non-React frameworks. Usually you won’t call this directly unless you have a very custom setup.
import React from 'react';
import { createNeutrixStore } from 'neutrix';
interface CounterState {
count: number;
}
const { useStore, store } = createNeutrixStore<CounterState>({ count: 0 });
function Counter() {
const count = useStore(s => s.count);
function increment() {
store.set('count', count + 1);
}
return (
<div>
<p>Count is {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
useCounterStore.store.use({
onSet: (path, value, prevValue) => {
console.log(`Changing ${path}: ${prevValue} -> ${value}`);
return value;
},
onGet: (path, value) => {
console.log(`Reading ${path}: ${value}`);
return value;
}
});
function doSomeAsyncStuff(state: CounterState) {
return new Promise<Partial<CounterState>>(resolve => {
setTimeout(() => {
resolve({ count: state.count + 10 });
}, 1000);
});
}
const { useStore, store } = createNeutrixStore({
count: 0,
doSomeAsyncStuff // <= function in initial state
});
// in a component:
function BigIncrement() {
const { doSomeAsyncStuff } = store.getState();
async function handleClick() {
await doSomeAsyncStuff();
}
return <button onClick={handleClick}>Increment by 10</button>;
}
Neutrix tracks which state paths you read in a computed or subscription. Only those paths trigger re-renders. No manual memo or “array of dependencies” is needed.
If you pass devTools: true
in your options, Neutrix will connect to Redux DevTools automatically:
const { useStore } = createNeutrixStore(
{ count: 0 },
{ devTools: true, name: 'CounterStore' }
);
To create SSR-friendly stores, you can use createCoreStore or a specialized SSR approach. For example:
export function createStoreForSSR(initialState) {
const store = createCoreStore(initialState);
return {
store,
getServerSnapshot: () => JSON.stringify(store.getState()),
rehydrate: (snapshot: string) => {
const parsed = JSON.parse(snapshot);
Object.keys(parsed).forEach(key => {
store.set(key, parsed[key]);
});
}
};
}
- LRU Caching for computed values.
- Proxy-based dependency tracking—only re-renders where needed.
- Batching: You can batch multiple set() calls to avoid extra re-renders.
-
Q: Do I need a Provider for everything?
-
A: No! If you prefer something like Zustand, just use
createNeutrixStore
without{ provider: true }
. If you need multiple stores or SSR, pass{ provider: true }
. -
Q: How do I handle side effects or async logic?
-
A: You can define an async function in your store’s initial state or call store.action(fn). Both let you do async updates seamlessly.
-
Q: Does Neutrix replace Redux entirely?
-
A: Not necessarily. Neutrix can handle most use cases with less boilerplate, but Redux might still be used if you’re heavily invested in its ecosystem.
We love contributions! Please see our contributing guide for details.
MIT
Built with ❤️ by oha