Kin Store

Start minimal. Grow structured.

A tiny, framework-agnostic reactive state library for TypeScript.

# Vanilla projects.
npm add @kintools/store-core
 
# React projects (includes core).
npm add @kintools/store-react
 
# Optional plugins.
npm add @kintools/store-plugins

createStore

A value plus get, set, merge, subscribe. Logic can live in plain functions that call them, no framework required.

import { createStore } from "@kintools/store-core";
 
const todos = createStore({
  items: [] as string[],
  status: "idle" as "idle" | "loading",
});
 
function addTodo(text: string): void {
  todos.merge((s) => ({ items: [...s.items, text] }));
}
 
async function fetchTodos(): Promise<void> {
  todos.merge({ status: "loading" });
  const items = await api.fetchTodos();
  todos.set({ items, status: "idle" });
}

Add structure with .use()

When a store earns it, each .use() registers a plugin: a declarative object that defines methods and lifecycle hooks. Plugins can be top-level or namespaced. Nothing wraps or patches the store underneath.

import { createStore } from "@kintools/store-core";
import { devtools, persist } from "@kintools/store-plugins";
 
const todos = createStore({
  items: [] as string[],
  status: "idle" as "idle" | "loading",
})
  .use({
    addTodo(text: string): void {
      this.merge((s) => ({ items: [...s.items, text] }));
    },
    async fetchTodos(): Promise<void> {
      this.merge({ status: "loading" });
      const items = await api.fetchTodos();
      this.set({ items, status: "idle" });
    },
  })
  .use(devtools({ name: "todos" }))
  .use("persist", persist({ key: "todos" }));
 
todos.addTodo("Buy milk"); // Full intellisense.
await todos.persist.hydrationComplete(); // Full intellisense.

React

useStore and useSelector subscribe a component to a store or a slice of it, built on useSyncExternalStore. useSelector re-renders only when the selected value changes.


@kintools/store-react re-exports the core API, so a React app needs a single install. See the React bindings.


Methods are called directly on the store, like todos.addTodo() in the handler: no hooks or subscriptions needed.

import { createStore, useSelector } from "@kintools/store-react";
 
const todos = createStore({ items: [] as string[] }).use({
  addTodo(text: string): void {
    this.merge((s) => ({ items: [...s.items, text] }));
  },
});
 
function TodoList() {
  const items = useSelector(todos, (s) => s.items);
  return (
    <>
      <ul>
        {items.map((item) => <li key={item}>{item}</li>)}
      </ul>
      <button onClick={() => todos.addTodo("Buy milk")}>Add</button>
    </>
  );
}

derive

Compose one or more stores into a lazy, dependency-tracked, read-only view. It tracks only the stores you call get() on: no selector arrays, no dependency graph to maintain by hand.

import { derive } from "@kintools/store-core";
 
const itemCount = derive((get) => get(todos).items.length);
 
itemCount.subscribe(function () {
  console.log(this.get());
});
 
todos.addTodo("Buy milk"); // logs 1

How it compares

Kin StoreZustandRedux / RTKJotaiMobX
Bundle size1.6 KB389 B17.5 KB4.0 KB15.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.

For full comparison, see the details →