resso: a React store object that is also its own setter
🪢 The simplest React state manager
At a glance
- What is it?
- resso is a React state manager built around a single mutable store object that you read with store.useStore() and write with the same object, and its pitch is small size and re-rendering only the components whose fields changed. It requires React 18 or newer and its rules changed sharply in v0.29.0.
- Who is it for?
- resso suits a React 18 or newer application that has one or two pieces of shared state and a preference for a store you can read and write without action types, dispatchers or selector plumbing, and it suits a team that will read the breaking change notes before upgrading. It does not suit a large codebase with many state domains, an application that must run on React 17, or a team that wants the discipline of a reducer and an action log.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 16 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One object that holds state and changes it
The whole model fits in a short example. You create a store from a plain object of initial values, and the object you pass in is the store you keep:
import resso from 'resso';
const store = resso({
count: 0,
text: 'hello',
inc() {
store.count += 1;
},
});Note that inc refers to store while the store is still being defined. That works because the method is called later, after the binding exists, and it is the shape that makes the library feel like plain JavaScript rather than a framework. The store carries both the data and the actions, and an action is just a method on the store.
The setter side is where the design gets unusual, because the store is callable. You can assign a field directly, call the store with a single key and an updater function, or call it with an object or a function that returns an object:
// Single
store.count = 60;
store('count', (c) => c + 1);
// Multiple
store({
count: 60,
text: 'world',
});
store((s) => ({
count: s.count + 1,
text: s.text === 'hello' ? 'world' : 'hello',
}));So a store is simultaneously a property bag, a mutator, and a function. The distinction that the README draws is between reading state and setting it. Direct access, store.count, is a data read. Assignment, store.count = 60, and the call forms are what trigger a re-render.
The name is an acronym, Reactive Elegant Shared Store Object, and the topic tags on the repository list the state management libraries it positions itself against: flux, redux, jotai, recoil, valtio and zustand. The claim is not that those are wrong, but that there is a shorter path to the same result.
In components you must go through useStore
The rule that catches people is stated in a note near the top of the README, under breaking changes since v0.29.0. To better fit AI and React Compiler, in React components you should always use const { count } = store.useStore() to get state. Direct access, store.count or const { count } = store, is now for pure data reads.
That is a meaningful change in behaviour, not a style preference. Before v0.29.0, destructuring the store directly gave you a reactive value. After it, destructuring gives you a snapshot that is not tracked, and the only tracked path in a component is the useStore hook. The same note also states that only react >= 18 is supported.
The reason the library gives for the change is worth repeating because it explains the direction of travel: the goal is compatibility with the React Compiler and with code written by AI assistants, both of which want the reactive value to come from a hook call rather than from an object property that happens to be reactive today. If you are reading this because your app stopped updating after an upgrade, this is why, and the fix is mechanical: replace every store.count read inside a component with a destructured value from store.useStore().
Outside components the rule is the opposite. In a method, reading store.count directly is correct, because there is nothing to re-render. The API section draws exactly this line, with the comment Get state, in component for the hook form and Read state, in method for the direct form.
Re-render on demand means only the fields you read
The library's headline feature is auto-handled on-demand re-render, and the README demonstrates it with three components sharing one store.
// No `text` update, no re-render
function Text() {
const { text } = store.useStore();
return <p>{text}</p>;
}
// Only `count` update triggers re-render
function Count() {
const { count } = store.useStore();
return <p>{count}</p>;
}
// No state in UI, no re-render
function Control() {
return (
<>
<button onClick={store.inc}>+</button>
<button onClick={() => (store.count -= 1)}>-</button>
</>
);
}The mechanism is field level tracking. A component that reads text through useStore subscribes to text, so changing count does not touch it. A component that reads nothing subscribes to nothing, so it never re-renders, and that is why the decrement button in Control can write store.count directly without subscribing.
That last part is the design in miniature: writes and reads are separated, and the subscription is decided by what a component destructures. It also means the shape of your destructuring is the shape of your re-render behaviour, so a component that destructures a large object pays for the fields it asked for rather than for the whole store.
The bundle size badge in the README shows 690 B, which is the number that makes this design credible. Re-render granularity of this kind normally costs code, and here the whole package is small enough to fit inside a fraction of a kilobyte.
Installing it and writing the first store
The package is published to npm as resso and can be added with any of the four major package managers:
bun add resso
#
pnpm add resso
#
yarn add resso
#
npm i ressoThe component side is the standard useStore hook, and the actions come along with the state in the same object:
function App() {
const { count, inc } = store.useStore();
return (
<>
{count}
<button onClick={inc}>+</button>
<button onClick={() => (store.count -= 1)}>-</button>
</>
);
}Note what is not required. There is no provider component, no reducer, no action constants, and no dispatch. The store is imported as a module level value, which is why the same store can be read from a method, a component or a utility file without any context wiring.
The README also has a section on shared refs, which it is careful to say is not related to resso and is just JavaScript. You export an object of plain values from a module and mutate it wherever you like, with no reactivity at all. It is included because beginners ask how to share mutable data between modules, and the answer is that sometimes you do not need a store for that.
There is also a demo on CodeSandbox, linked from the README, which is the fastest way to see the behaviour in a running app before installing anything.
What ships in the package
The package manifest is small and the published surface is one entry point. The exports map has a single key, with types pointing at ./dist/resso.d.ts, require at ./dist/resso.cjs.js and import at ./dist/resso.esm.js, and the files array ships only dist. There is no subpath export, no plugin entry point and no runtime dependency other than React itself, which is declared as a peer dependency at >=18.0.0.
That peer dependency range is the reason for the react >= 18 note in the README, and it also means resso can sit in an application that already owns its React version rather than pinning one.
TypeScript support is first class rather than an afterthought. The types field points at a generated declaration file, the repository ships an npm types badge, and the type definitions live in src/index.ts according to the badge link. The build pipeline reflects the same: rollup with the TypeScript plugin produces the bundle and the declaration files, rollup-plugin-dts handles the type output, and the build script removes dist and coverage before running rollup.
The development tooling is modern and self hosted. Tests run under vitest with jsdom and the React testing library, coverage comes from the v8 provider, and linting and formatting are done with oxlint and oxfmt rather than the eslint and prettier combination, which keeps the toolchain short. The repository also carries a .oxlintrc.json and an .oxfmtrc.json, a tsconfig.json, a vitest.config.mts and a bun.lock, so the project itself is developed with bun.
Version 0.29.0 with no published GitHub releases
Two facts about the release record deserve to be read together.
The first is that the repository has no GitHub releases. The npm package is the distribution channel, and the version in package.json reads 0.29.0, which is the same version the breaking changes note refers to. That means there is no GitHub release page to consult for notes; the changelog lives in commit history or in the Medium article the README links, which is titled resso, world's simplest React state manager.
The second is that this is a 0.x version with a 0.x set of promises. A breaking change landed in v0.29.0 and the README documents it as a note, not as a migration guide. The topics list the library next to redux, jotai, recoil, valtio and zustand, and the shared vocabulary of that family includes breaking changes in the zero range.
On the positive side, the repository is not archived and the last push was on 2026-10-07, so the code is still being worked on. The licence is MIT, and the README points to the LICENSE file for the terms.
For an evaluator the practical question is what happens when the API moves again. Because the state access rule already changed once in the 0.x line, the safe assumption for any version of resso is that a minor bump may change how you read state, and the upgrade step is a search and replace over your components rather than a config change.
Against the reducer and dispatch model
The alternative worth comparing is not another package but a different shape of state management, the reducer and dispatch model that the flux and redux tags refer to. The difference in approach is specific.
In a reducer model, state is immutable. Updates are expressed as actions, a reducer describes the transition, and the store dispatches those actions. Every change is therefore described somewhere, which means the set of possible transitions is enumerable and a debugger can show them. The cost is ceremony: action types, action creators, dispatch calls, and the indirection between a call site and the state it changes.
resso inverts that. The state is a mutable object, the object is the setter, and a change is a plain assignment or a call. There is no layer between the code that wants to change something and the change itself. What you give up is the description of transitions, since store.count = 60 records nothing about intent, and what you gain is that a component, a plain function and an event handler all change state the same way, with the same object.
There is also the atom based alternative, where state is split into small units and each has its own subscription. That is the closest cousin to what resso does, and the difference is granularity of the unit: resso exposes one store object with field level subscriptions, whereas an atom based approach makes the unit itself the thing you subscribe to. The README does not benchmark the two against each other, so the honest summary is that resso chooses a smaller mental model, one object and a hook, over a more granular one.
For a codebase with a dozen components and one shared counter, resso's model is a clear win in line count. For a codebase with several state domains and a team that values being able to list every possible state change, the reducer model is the better fit, and no amount of bundle size makes up for the difference.
Editorial conclusion
resso suits a React 18 or newer application that has one or two pieces of shared state and a preference for a store you can read and write without action types, dispatchers or selector plumbing, and it suits a team that will read the breaking change notes before upgrading. It does not suit a large codebase with many state domains, an application that must run on React 17, or a team that wants the discipline of a reducer and an action log. Verify first by checking whether your components read state only through store.useStore(), because the direct access form, store.count, is explicitly for pure data reads after v0.29.0 and using it for rendering is the mistake that produces stale output.
Frequently asked questions
How do I install resso?
The package is published to npm and can be added with bun add resso, pnpm add resso, yarn add resso or npm i resso. React 18 or newer is required as a peer dependency.
How do I read state in a React component with resso?
Always use const { count } = store.useStore() inside a component. Since v0.29.0 direct access such as store.count or const { count } = store is for pure data reads, and the change was made to better fit AI and the React Compiler.
How do I update state with resso?
Assign directly with store.count = 60, call the store with a key and updater with store('count', (c) => c + 1), or pass an object or a function returning an object for multiple fields. Writes in these forms trigger the re-render.
Does resso re-render every component when state changes?
No. A component re-renders only when a field it read through store.useStore() changes, and a component that reads no state does not re-render at all. The README shows separate Text and Count components that ignore each other's updates.
Is resso suitable for React 17 projects?
No. The README states that only react >= 18 is supported, and the package manifest declares React >=18.0.0 as a peer dependency.
What is the licence of resso?
The project is released under the MIT Licence, with the copyright line pointing to nanxiaobei and the LICENSE file at the root of the repository holding the terms.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nanxiaobei-resso)Was this useful?