I Built a Tiny State Management Library in TypeScript atostate
State management is one of those topics every frontend developer eventually bumps into.
At first, everything is simple:
- local state
- props
- maybe a few shared variables
Then the app grows…
and suddenly state is everywhere, duplicated, out of sync, and hard to reason about.
Big libraries solve this but often with boilerplate, mental overhead, or framework lock-in.
So I decided to build my own minimal solution.
Meet atostate 👋
What is atostate?
atostate is a tiny tool state management library for JavaScript and TypeScript.
Its goal is very simple:
Provide a predictable global store with subscriptions and strong typing without complexity.
No framework assumptions.
No magic.
Just state + rules.
Why I built it
I wanted a library that:
- Is small and readable
- Works in vanilla JS or TS
- Doesn’t force Redux-style architecture
- Is easy to debug and reason about
Creating a store
import createStore from 'atostate';
type State =
count: number;
;
const store = createStore<State>(
count: 0,
);
That’s it.
You now have a global store.
Updating state
State updates are explicit and predictable:
store.setState( count: 1 );
store.setState((prev) => (
...prev,
count: prev.count + 1,
));
No direct mutation.
No hidden side effects.
Subscribing to changes
Subscribe to all state changes:
store.subscribe(() =>
console.log('State changed:', store.getState());
);
Or subscribe to only what you care about:
store.subscribe(
(state) => state.count,
(count, prev) =>
console.log('Count changed:', prev, '→', count);
);
This makes updates efficient and intentional.
Selectors + equality checks
Subscriptions only re-run when their selected slice actually changes.
import shallowEqual from 'atostate';
store.subscribe(
(state) => state.user,
(user) =>
console.log('User changed:', user);
,
shallowEqual
);
No unnecessary re-runs.
No wasted work.
Actions: organizing logic cleanly
To keep logic out of UI code, atostate provides a lightweight actions API.
const counter = store.actions(( setState ) => (
increment()
setState((s) => ( ...s, count: s.count + 1 ));
,
reset()
setState( count: 0 );
,
));
counter.increment();
counter.reset();
Simple, explicit, and easy to test.
Optional Redux-style reducers
If you like reducers and dispatch, atostate supports that too optionally.
type Action =
| type: 'inc'
| type: 'set'; value: number ;
function reducer(state: State, action: Action): State
switch (action.type)
case 'inc':
return ...state, count: state.count + 1 ;
case 'set':
return ...state, count: action.value ;
default:
return state;
const store = createStore<State, Action>(
count: 0 ,
reducer
);
store.dispatch( type: 'inc' );
Use it only if you want it.
Middleware support
Cross-cutting concerns are handled via middleware.
Logger
import loggerMiddleware from 'atostate';
createStore(
count: 0 ,
middleware: [loggerMiddleware('app')]
);
Persistence
import persistMiddleware from 'atostate';
createStore(
count: 0 ,
middleware: [persistMiddleware('app-state')]
);
TypeScript-first
atostate is written entirely in TypeScript:
- Typed state
- Typed actions
- Typed selectors
- Great autocomplete
No any. No guessing.
What atostate is not
atostate intentionally avoids:
- Framework-specific APIs
- Heavy abstractions
- Hidden reactivity
- Large surface area
It’s a core state engine, not a full framework.
When should you use it?
atostate is a good fit if you want:
- A small global store
- Shared state across components
- Strong typing
- Minimal learning curve
- Full control over architecture
If you enjoy understanding how your tools work and don’t want unnecessary complexity you might enjoy using it.
Feedback, ideas, and PRs are very welcome 🙌
