Super simple state management for React apps.
- Global singleton store +
useSyncExternalStore, noProvider/ noContextneeded. - Modular stores:
state/getters/actions/watchper module. - Nested path read/write via
nx.$get/nx.$set(e.g.user.profile.login). - Global
nx.$get / $set / $calloutside components, plusnx.$usehook inside components. - Side-effect import: attaches the
$-prefixed APIs onto the globalnx(@jswork/next).
npm install @jswork/react-tiny-state
// stores/modules/theme.ts import '@jswork/next'; export default nx.$defineStore({ state: { primary: 'green', child: { n1: { a: 123 } }, }, watch: { primary: (newValue, oldValue) => { console.log('theme primary changed:', newValue, oldValue); }, 'child.n1.a': (newValue, oldValue) => { console.log('nested value changed:', newValue, oldValue); }, }, });
// stores/modules/user.ts import '@jswork/next'; export default nx.$defineStore({ state: { profile: null, session: null, }, getters: { isLogin(state) { return !!state.profile?.login; }, }, actions: { async profile({ username }) { const res = await fetch(`https://api.github.com/users/${username}`).then((r) => r.json()); nx.$set('user.profile', res); }, }, });
// stores/index.ts (Vite glob auto registration) import nx from '@jswork/next'; import '@jswork/react-tiny-state'; export const stores = nx.$registerStores( import.meta.glob('./modules/*.ts', { eager: true, import: 'default' }) ); export default stores;
You can also manually call
nx.$defineStore(descriptor, autoName)per module. Each module key is camelized (camelize) when merged into the root state, sotheme/userare accessed vianx.$get('theme.*')/nx.$get('user.*').
No Provider / no Context — just make sure the store modules are imported once in your app entry:
import '@jswork/next'; import './stores'; // registers every module onto the global store export default function App() { return <Comp1 />; }
const theme = nx.$get('theme'); // read whole module state const isLogin = nx.$get('user.isLogin'); // read getter const profile = nx.$get('user.profile.login'); // read nested path // update state (triggers watchers + re-render) nx.$set('theme.primary', 'red'); nx.$set('user.profile', { login: 'afeiship' }); // call an action (this inside action = module state) nx.$call('user.profile', { username: 'afeiship' });
This package has no default export. Importing it attaches the $-prefixed APIs onto the global nx (@jswork/next); it also re-exports the related types (StoreModuleDef, Watcher, ActionFn, GetterFn).
import '@jswork/next'; import '@jswork/react-tiny-state'; // side effect: nx.$defineStore / $get / $set / ... are ready
// 1. Name as first argument (Pinia style) nx.$defineStore('counter', { state: { count: 0 }, getters: { double: (state) => state.count * 2 }, actions: { increment: (state) => nx.$set('counter.count', state.count + 1) }, }); // 2. Name inside descriptor nx.$defineStore({ name: 'theme', state: { primary: 'green' }, watch: { primary: (newVal, oldVal) => console.log(newVal, oldVal) } });
Batch register modules imported via Vite's import.meta.glob. Supports nested subdirectories and strips /index:
nx.$registerStores(import.meta.glob('./modules/**/*.ts', { eager: true, import: 'default' }));
modules/counter.ts->countermodules/member/profile.ts->memberProfilemodules/member/orders.ts->memberOrdersmodules/member/index.ts->member
State fields are proxied so assignment like this.profile = res works internally; getters are exposed as getter properties on the module state (read them via nx.$get('user.isLogin')).
Read state synchronously anywhere, e.g. nx.$get('theme.primary'), nx.$get('user.profile.login', null).
Write a nested path (supports dotted keys) and dispatch the update. Fires the matching watch watcher of that module.
Call an action: nx.$call('user.profile', { username: 'afeiship' }). Inside the action, this refers to the module state.
React hook version of $get, usable inside components: const count = nx.$use('theme.primary');.
Code released under the MIT license.