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.