React
React bindings for @kin-store/core.
Install
npx jsr add @kin-store/reactpnpm add jsr:@kin-store/reactdeno add jsr:@kin-store/react@kin-store/react depends on and re-exports everything from @kin-store/core, so no need to install it separately.
useStore
Subscribes a component to a store's whole state and re-renders on every state change. Backed by useSyncExternalStore, safe for concurrent mode.
import { createStore, useStore } from "@kin-store/react";
const counter = createStore(0);
function Counter(): JSX.Element {
const count = useStore(counter);
return <div>{count}</div>;
}Works with any store, createStore, withPlugins, or derive:
const summary = derive((get) => ({
greeting: `Hello, ${get(userStore).name}`,
itemCount: get(cartStore).items.length,
}));
function Header() {
const { greeting, itemCount } = useStore(summary);
return (
<header>
{greeting} — {itemCount} items
</header>
);
}To subscribe to a transformed value derived from the state, use useSelector instead.
useSelector
Selects a transformed value from the state and re-renders only when that value changes, using an equality function to decide whether it actually changed. Defaults to shallowEqual, which compares the value one level deep, safe even when the selector returns a new object or array reference on every call (e.g. .filter(), .map(), object literals):
import { useSelector } from "@kin-store/react";
// Only re-renders when `name` changes, not on every state update.
function UserName(): JSX.Element {
const name = useSelector(userStore, (s) => s.name);
return <span>{name}</span>;
}
function ActiveTodos(): JSX.Element {
// shallowEqual (the default) prevents a re-render when the filtered
// list's contents haven't changed, even though .filter() returns a new
// array reference every call.
const active = useSelector(
todoStore,
(s) => s.items.filter((item) => !item.completed),
);
return (
<ul>
{active.map((t) => <li key={t.id}>{t.title}</li>)}
</ul>
);
}Pass a custom equality function for cases shallowEqual can't cover, like tolerance-based comparisons:
const progress = useSelector(
downloadStore,
(s) => s.bytesLoaded / s.totalBytes,
(a, b) => Math.abs(a - b) < 0.001,
);shallowEqual is also exported on its own, for use outside this hook.
StoreProvider and useStoreContext
Inject a store via React context, useful for testing or SSR where you want to avoid module-level singletons:
import {
StoreProvider,
useStore,
useStoreContext,
withPlugins,
} from "@kin-store/react";
const store = withPlugins(0).use({
reducers: {
increment: (state, n: number) => state + n,
},
});
type Store = typeof store;
function App(): JSX.Element {
return (
<StoreProvider store={store}>
<Counter />
</StoreProvider>
);
}
function Counter(): JSX.Element {
const store = useStoreContext<Store>();
const count = useStore(store);
return <button onClick={() => store.dispatch.increment(1)}>{count}</button>;
}useStoreContext throws if called outside a <StoreProvider> tree.
Actions are stable refs
Methods and dispatch functions on a withPlugins store are stable references, they don't change between renders. You can call them directly without subscribing:
function AddButton() {
// No useStore/useSelector needed — just call the method directly.
return <button onClick={() => todoStore.addTodo("new item")}>Add</button>;
}