Start with a plain store. Add structure only when the app earns it.
A framework-agnostic reactive state library for TypeScript.
Most state libraries pick your architecture before you know if the app needs one: actions, reducers, selectors, a provider tree, decided on day one. Kin Store leaves that decision to you.
set and dispatch are equally first-class, not a beginner tier and an advanced one, so the mutation style a store uses is a choice your team makes, not one the library makes for you.
createStore231 Bget, set, subscribe. Nothing else.
withPlugins1.0 KBAdd methods, reducers, and middleware, one .use() at a time.
derive438 BCompose stores into new ones. It tracks what you read, not a graph you maintain.
A store starts as get, set, subscribe, nothing else. Methods, reducers, middleware, and derived stores are things you add when you reach for them, not things you start with.
No proxies, no auto-tracked reactive graph, no immer unless you add it. State only changes where you called set or dispatch.
Each plugin declares what it adds. Stack ten of them and the chain still reads top-to-bottom, nothing nested to unwind.
derive tracks which stores you read automatically. No selector library, no dependency array to keep in sync by hand.
| Kin Store | Zustand | Redux / RTK | Jotai | MobX | |
|---|---|---|---|---|---|
| Bundle size | 2.0 KB | 389 B | 17.5 KB | 4.0 KB | 15.6 KB |
| Zero dependencies | ✅ | ✅ | ❌ | ✅ | ✅ |
| 100% type-safe | ✅ | ⚠️ | ⚠️ | ✅ | ✅ |
| Low boilerplate | ✅ | ⚠️ | ❌ | ⚠️ | ⚠️ |
| Separate state and logic | ✅ | ❌ | ✅ | — | ✅ |
| Opt-in complexity | ✅ | ✅ | ❌ | ⚠️ | ❌ |
| No hidden magic | ✅ | ✅ | ✅ | ✅ | ❌ |
✅ full support · ⚠️ partial or conditional · — not applicable (different model)
Bundle sizes are each library's full package import, bundled with rolldown, minified, and gzipped. Tree-shaking down to only the APIs you use will land smaller across the board.
Kin Store is new: this table is accurate today, but Redux, Zustand, Jotai, and MobX all carry years of production use this library doesn't have yet. Try it, and tell us where it breaks.
For full comparison, see the details →
01 Declare
import { createStore } from "@kin-store/core";
const count = createStore(0);
const theme = createStore<"light" | "dark">("light");
type TodoState = {
items: string[];
status: "idle" | "loading";
};
const todos = createStore<TodoState>({
items: [],
status: "idle",
});02 Read, write, subscribe
count.set((n) => n + 1);
theme.set("dark");
todos.set((s) => ({ ...s, items: [...s.items, "Buy milk"] }));
console.log(count.get()); // 1
const unsubscribe = count.subscribe((get, prev) => {
console.log(prev, "->", get());
});
count.set((n) => n + 1); // logs "1 -> 2"
unsubscribe();03 Compose
import { derive } from "@kin-store/core";
const itemCount = derive((get) => get(todos).items.length);
console.log(itemCount.get()); // 104 When the store earns it, add structure
import { withPlugins } from "@kin-store/core";
import { devtools, persist } from "@kin-store/plugins";
const store = withPlugins(todos)
.use("persist", persist({ key: "todos" }))
.use("devtools", devtools())
.use({
// A plugin is a plain object: methods/reducers/middleware, nothing
// wraps or patches the store to add them.
methods: (store) => ({
addTodo(text: string): void {
store.set((s) => ({ ...s, items: [...s.items, text] }));
},
async fetchTodos(): Promise<void> {
store.set((s) => ({ ...s, status: "loading" }));
const items = await api.fetchTodos();
store.set({ items, status: "idle" });
},
}),
});
await store.persist.hydrate(); // From the namespaced persist plugin.
store.addTodo("Buy milk"); // From the top-level inline plugin.05 Need traceability? Add reducers and replace set by dispatch for those changes
const store = withPlugins(todos)
.use("persist", persist({ key: "todos" }))
.use("devtools", devtools())
.use({
reducers: {
addTodo: (s, text: string) => ({ ...s, items: [...s.items, text] }),
fetchStart: (s) => ({ ...s, status: "loading" }),
fetchDone: (_s, items: string[]) => ({ items, status: "idle" }),
},
methods: (store) => ({
async fetchTodos(): Promise<void> {
store.dispatch.fetchStart();
const items = await api.fetchTodos();
store.dispatch.fetchDone(items);
},
}),
});
store.dispatch.addTodo("Buy milk"); // Full intellisense, logged in devtools.set/dispatch are both first-class here: pick whichever fits this store or method, not a ladder from one to the other.
In React
import { useSelector, useStore } from "@kin-store/react";
function Counter(): JSX.Element {
// Re-renders on every change. Works great for primitive stores.
const value = useStore(count);
return <button onClick={() => count.set((n) => n + 1)}>{value}</button>;
}
function TodoList(): JSX.Element {
// Re-renders only when items changes.
const items = useSelector(store, (s) => s.items);
return (
<ul>
{items.map((item) => <li key={item}>{item}</li>)}
{/* Direct method reference. No hook, no subscription. */}
<button onClick={() => store.addTodo("Buy milk")}>Add</button>
</ul>
);
}