
gittech. site
for different kinds of informations and explorations.
I made a state management library
đ Neutrix
A powerful and hopefully simple state management library for React.
Docs!
Docs can be found here: https://duriantaco.github.io/neutrix
Table of Contents
- Overview
- Installation
- Motivation
- Quick Decision Guide
- Usage
- API Reference
- Examples
- Advanced Topics
- FAQ
- Contributing
- License
Overview
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.
Installation
npm install neutrix
or
yarn add neutrix
Motivation
The Problem
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.
The Solution
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.
Quick Decision Guide
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>
);
}
Usage
1. Quick Start (Recommended)
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.
2. Single Store without provider
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
3. Single Store With Provider
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.
4. Multiple Stores
You can create multiple store instances (e.g. userStore, cartStore) and combine them under a single
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>;
}
API Reference
createNeutrixStore
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
Description:
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
createCoreStore
(Advanced)
function createCoreStore<T extends State>(
initialState: T,
options?: StoreOptions
): Store<T>;
Description:
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.
Examples
Simple counter
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>
);
}
Using middleware:
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;
}
});
Async actions:
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>;
}
Advanced Topics
Automatic dependency tracking
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.
Dev-tools integration
If you pass devTools: true
in your options, Neutrix will connect to Redux DevTools automatically:
const { useStore } = createNeutrixStore(
{ count: 0 },
{ devTools: true, name: 'CounterStore' }
);
SSR Usage
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]);
});
}
};
}
Performance Optimizations
- 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.
FAQ
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.
Contributing
We love contributions! Please see our contributing guide for details.
License
MIT
Built with â¤ď¸ by oha