A simple, efficient, and robust reactive data store to help write decoupled code.
It doubles as a learning resource through the provided samples you can run from the GitHub demos or your local clone.
A typical case is in frontend development between UI and data dependencies: UI components describe what slice of data they care about, the store notifies them when that slice changes, keeping the logic encapsulated on each component.
- Updates only the UI affected by a state change.
- No virtual DOM or browser dependency, can be used too for UI-less scenarios.
- Allows UI subscribers to use existing DOM nodes instead of recreating the component HTML.
- Batches consecutive updates on the next frame draw or microtask.
- Subscribers always receive a settled state.
- Works via
importor<script>. - No runtime dependencies.
- Core store logic is about 100 lines of code (≈700 B minified and gzipped).
It's meant to stay small, for simple and medium-complexity scenarios. For larger-scale frameworks with similar store concepts and more features see Solid, Preact signals, Vue etc.
Used in production by tutorforme.org.
Fun fact: I first implemented this pattern in 1997 in C++ for "Microsoft Money" reactive UI updates from database changes. It was the first Microsoft program using reactive UI!
The store implementation lives in src/store.js with an ES module wrapper in src/store.module.js. The public API is:
Including src/store.js with a plain <script> tag makes the API available as StoreLib.createStore(...). In module-aware environments use import { createStore } from './src/store.module.js';.
const store = createStore(initialState); //returns an object with `get`, `set`, `patch`, and `subscribe`.
store.get(); //returns the latest snapshot of state.
store.set(newState); //replaces the state and queues notifications.
store.patch(partialState); //shallow merges state and queues notifications.
store.subscribe(callback, selector?); //registers a listener. The callback runs immediately with the initial selected value and only runs again when that value changes.Internally, tinyReactive keeps a Set of subscribers. Each subscriber caches the last value from its selector, so updates fire only when needed. Notifications are deferred with requestAnimationFrame (or queueMicrotask/setTimeout outside the browser) to ensure all mutations settle before DOM work. Selector or subscriber failures are caught, logged, and unsubscribed so a single bug cannot stall the store or provide an inconsistent state.
You can inspect and debug samples directly from the demos in GitHub Pages, or clone the repo:
git clone https://github.com/zmandel/tinyreactive.git
cd tinyreactiveThe project is framework-free; open the minimal demo from your file explorer at samples/minimal/index.html directly in your browser (file:// protocol)
For richer examples such as samples/tasks-app, run a local dev server (import modules need http).
cd samples/tasks-app
npm install
npm run devImport the store library (import or a <script>) and wire it to your UI code:
<div id="count"></div>
<button>Increment</button>
<script type="module">
import { createStore } from './src/store.module.js';
// Create a store with a primitive initial value.
// Note: for complex states, use objects such as { count: 0, otherProp: true }.
const store = createStore(0);
// Subscribe to the entire store, since its just one value.
// Note: for complex states, use selectors to subscribe to slices: state => state.count (see tasks-app sample)
store.subscribe(value => {
document.querySelector('#count').textContent = value;
});
document.querySelector('button').addEventListener('click', () => {
store.set(store.get() + 1);
});
</script>In this case, the state is just a number primitive. When its an object, it detects changes with an internal shallow object comparison of the selector slice, using the included helper valuesEqual(a,b).
These samples can be run and debugged directly from the demos below.
samples/minimalwires a counter to UI. The subscription renders the count, and the click handler only sets the new count.samples/tasks-appView store events from a notification panel (👁️) as state changes propagate to the UI. Independent subscriptions render the lists, summary, filter buttons, and notification panel. Selectors such asstate => state.todoskeep updates targeted to only what changed.
Inspect the running examples directly:
Open the devtools, set breakpoints, and watch how state changes travel through selectors into the UI.
Issues and pull requests are welcome.
This project is licensed under the MIT License.
