Design Principles
These four principles shaped every API decision in Kin Store. Understanding them makes the library predictable, and explains why things work the way they do.
Explicit over implicit
No hidden merges, no auto-propagating destroy, no magic dependency graphs. If something happens, you triggered it.
set replaces the entire state — there is no shallow merge happening behind the
scenes. derive tracks only the stores you explicitly read with get(store).
destroy must be called manually — nothing propagates to child stores
automatically. The CANCELED sentinel, named reducers, and the two-tier
mutation model all follow from this principle.
Opt-in complexity
createStore is the floor. withPlugins adds methods, reducers, middleware,
and lifecycle hooks, only when you import it. derive adds reactive
composition, only when you reach for it. You never pay for capability you
haven't opted into.
Type safety by default
Every reducer argument, dispatch call, and plugin method is fully inferred, no
any or unknown, no manual annotation at call sites. The type system is
load-bearing, not decorative.
dispatch.addTodo("Buy groceries") knows that addTodo takes a string. A
middleware that reads ctx.reducer.args gets the correct tuple type. A plugin
that adds methods sees the accumulated store type including every plugin
registered before it. Type errors are caught statically, at definition time or
at the call site.
Two tiers of mutation
dispatch.* and set are both first-class ways to change state, neither is
a fallback for the other. dispatch.* calls a named reducer through the
middleware pipeline, so the change is traceable, loggable, and cancellable.
set writes state directly, with no pipeline in between. Which one a team
reaches for is an architectural choice, not a hierarchy, and Kin Store is
deliberately built so any point on that spectrum is a first-class way to use
the library:
- Primitive composition —
createStore+derive+ plain functions, nowithPluginsat all. - Methods only —
withPlugins+methodsthat callsetdirectly, no reducers or middleware. - Reducers + middleware —
withPlugins+reducersdispatched throughdispatch.*, with middleware doing the logging/undo/guard work. - Fat store —
createStoreplus colocated top-level logic functions that callset, no plugin system involved.
Within a withPlugins store, a method can also mix both in the same call:
dispatch.* for the parts of a change that should be traceable, set for a
direct write, matching what that specific change needs rather than a
store-wide rule. If your team standardizes on one style — e.g. "every mutation
goes through dispatch.*" — that's a convention to hold at the module
boundary (export dispatch and your methods, not set, from your store
module), not something Kin Store enforces for you.