Skip to content

duriantaco/neutrix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🌀 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

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 , no context overhead, just a single store hook. Great for simpler apps.

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 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>;
}

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: If true, 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