
Tanstack Store
- 75 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
tanstack-store is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- tanstack-store
- AI & Agent Building
- AI-coding skill
Tanstack Store by the numbers
- 75 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,460 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill tanstack-storeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 75 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
TanStack Store
Overview
TanStack Store is a lightweight, framework-agnostic reactive state management library with type-safe updates, derived computations, batched mutations, and effect management. It powers the core of TanStack libraries internally and can be used as a standalone state solution. Framework adapters are available for React, Vue, Solid, Angular, and Svelte.
Core primitives:
- Store — reactive container with
setState, subscriptions, and lifecycle hooks - Derived — lazily computed values that track Store/Derived dependencies
- Effect — side-effect runner triggered by dependency changes
- batch — groups multiple updates so subscribers fire once
When to use: Shared reactive state across components, derived/computed values from multiple stores, batched state updates, framework-agnostic state logic reusable across React/Vue/Solid/Angular/Svelte, lightweight alternative to Redux/Zustand/MobX.
When NOT to use: Server state and caching (TanStack Query), complex normalized state with middleware (Redux Toolkit), form state management (TanStack Form), simple component-local state (useState/useSignal).
Key characteristics:
- Tiny bundle size with zero dependencies
- Immutable update model (always return new references from
setState) - Lazy evaluation for Derived values (recompute only when accessed after change)
- Explicit mount/unmount lifecycle for Derived and Effect (no automatic cleanup)
Installation
| Package | Use Case |
|---|---|
@tanstack/store | Framework-agnostic core |
@tanstack/react-store | React adapter (re-exports core) |
@tanstack/vue-store | Vue adapter |
@tanstack/solid-store | Solid adapter |
@tanstack/angular-store | Angular adapter |
@tanstack/svelte-store | Svelte adapter |
Framework packages re-export the core Store, Derived, Effect, and batch — install only the framework package, not both.
Quick Reference
| Pattern | API | Key Points |
|---|---|---|
| Create store | new Store(initialState, options?) | Generic over TState and TUpdater |
| Read state | store.state | Synchronous property access |
| Previous state | store.prevState | Value before last setState call |
| Update state | store.setState((prev) => newState) | Accepts updater function or direct value |
| Subscribe | store.subscribe(listener) | Returns unsubscribe function |
| Derived value | new Derived({ deps, fn }) | Lazily recomputes when dependencies change |
| Mount derived | derived.mount() | Required to activate dependency tracking |
| Derived from derived | Nest Derived in another deps | Forms a computation graph |
| Batch updates | batch(() => { ... }) | Subscribers notified once after all updates |
| Side effects | new Effect({ deps, fn, eager? }) | Runs fn when dependencies change |
| Mount effect | effect.mount() | Required to start listening |
| React binding | useStore(store, selector?) | Re-renders only when selected value changes |
| Shallow compare | useStore(store, selector, shallow) | Prevents re-renders for structurally equal objects |
| Lifecycle: subscribe | onSubscribe option | Fires on first subscriber, cleanup on last |
| Lifecycle: update | onUpdate option | Fires after every state change |
| Custom updater | updateFn option | Replace default setState behavior |
| Previous deps | fn: ({ prevDepVals }) | Compare current vs previous dependency values |
| Dep vals access | fn: ({ currDepVals }) | Array ordered by deps declaration order |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Reading derived.state without mounting | Call derived.mount() before accessing state |
| Forgetting to unmount derived/effect | Store the cleanup function and call it on teardown |
Multiple setState calls without batching | Wrap related updates in batch() to avoid intermediate recomputation |
Selecting objects in useStore without shallow | Pass shallow as third argument to prevent unnecessary re-renders |
| Mutating state object directly | Always return a new object from setState updater |
| Subscribing inside render without cleanup | Use useStore hook in React instead of manual subscribe |
| Creating stores inside React components | Instantiate stores outside components or in a ref/useState |
| Not including all deps in Derived | List every Store/Derived dependency in the deps array |
| Mounting Derived deps after the parent | Mount leaf dependencies before parent Derived values |
Using @tanstack/store with React directly | Install @tanstack/react-store which re-exports core |
Delegation
- Server state and caching: Delegate to the
tanstack-queryskill if available. Otherwise, recommend:npx skills add oakoss/agent-skills --skill tanstack-query - React component patterns: Delegate to framework-specific skills for component architecture
- Form state management: Delegate to the
tanstack-formskill if available. Otherwise, recommend:npx skills add oakoss/agent-skills --skill tanstack-form - Query pattern discovery: Use
Exploreagent to find examples in the codebase - Code review: Delegate to
code-revieweragent for store architecture review
References
- Core concepts: Store, setState, subscriptions, and lifecycle hooks
- Derived state: computed values, dependency tracking, and batching
- React integration: useStore hook, selectors, and performance
Core Concepts
Creating a Store
A Store wraps a value with reactive state management. Pass initial state and optional configuration.
import { Store } from '@tanstack/store';
const countStore = new Store(0);
const userStore = new Store({
name: 'Alice',
age: 30,
preferences: { theme: 'dark' },
});Reading State
Access the current value synchronously via the .state property. The previous value is available via .prevState.
console.log(countStore.state); // 0
countStore.setState(() => 5);
console.log(countStore.state); // 5
console.log(countStore.prevState); // 0Updating State
setState accepts an updater function that receives the previous state and returns the new state. It also accepts a direct value.
countStore.setState((prev) => prev + 1);
userStore.setState((prev) => ({
...prev,
name: 'Bob',
}));Always return a new object reference for object state. Direct mutation does not trigger subscribers.
// Wrong - mutates in place, no notification
userStore.setState((prev) => {
prev.name = 'Bob';
return prev;
});
// Correct - new object reference
userStore.setState((prev) => ({ ...prev, name: 'Bob' }));Subscriptions
Subscribe to state changes with a listener function. The listener receives the store instance. subscribe returns an unsubscribe function.
const unsubscribe = countStore.subscribe((store) => {
console.log('Count changed to:', store.state);
});
countStore.setState(() => 10); // logs: "Count changed to: 10"
unsubscribe();
countStore.setState(() => 20); // no logLifecycle Hooks
onSubscribe
Fires when the first listener subscribes. The returned function fires when the last listener unsubscribes.
const store = new Store(0, {
onSubscribe: (_listener, store) => {
console.log('First subscriber attached');
return () => {
console.log('All subscribers removed');
};
},
});
const unsub1 = store.subscribe(() => {});
// logs: "First subscriber attached"
const unsub2 = store.subscribe(() => {});
// no log (already has subscribers)
unsub1();
unsub2();
// logs: "All subscribers removed"onUpdate
Fires after every state update. Useful for side effects like logging or syncing to external systems.
const store = new Store(
{ count: 0 },
{
onUpdate: () => {
console.log('State updated:', store.state);
},
},
);
store.setState((prev) => ({ count: prev.count + 1 }));
// logs: "State updated: { count: 1 }"Custom Update Function
The updateFn option replaces the default setState behavior. It receives the previous state and returns a function that processes the updater.
const store = new Store(0, {
updateFn: (prev) => (updater) => {
const next = typeof updater === 'function' ? updater(prev) : updater;
return Math.max(0, next);
},
});
store.setState(() => -5);
console.log(store.state); // 0 (clamped to minimum)
store.setState(() => 10);
console.log(store.state); // 10Type-Safe Stores
Store is generic over TState. TypeScript infers the type from the initial value.
interface AppConfig {
apiUrl: string;
debug: boolean;
retryCount: number;
}
const configStore = new Store<AppConfig>({
apiUrl: 'https://api.example.com',
debug: false,
retryCount: 3,
});
configStore.setState((prev) => ({ ...prev, debug: true }));Installation
npm install @tanstack/storeFor framework adapters, install the framework-specific package instead:
npm install @tanstack/react-store
npm install @tanstack/vue-store
npm install @tanstack/solid-store
npm install @tanstack/angular-store
npm install @tanstack/svelte-storeDerived State
Creating Derived Values
Derived creates computed state from one or more Store or Derived dependencies. It recomputes lazily when dependencies change.
import { Store, Derived } from '@tanstack/store';
const firstName = new Store('John');
const lastName = new Store('Doe');
const fullName = new Derived({
deps: [firstName, lastName],
fn: ({ currDepVals }) => {
return `${currDepVals[0]} ${currDepVals[1]}`;
},
});Mounting and Cleanup
Derived values must be mounted to activate dependency tracking. mount() returns a cleanup function.
const unmount = fullName.mount();
console.log(fullName.state); // "John Doe"
firstName.setState(() => 'Jane');
console.log(fullName.state); // "Jane Doe"
unmount();Accessing .state on an unmounted Derived will not reflect dependency changes.
Dependency Value Access
The fn callback receives an object with current and previous dependency values.
const items = new Store([1, 2, 3]);
const multiplier = new Store(2);
const computed = new Derived({
deps: [items, multiplier],
fn: ({ currDepVals, prevDepVals }) => {
const [currentItems, currentMultiplier] = currDepVals;
// prevDepVals available for comparison
return currentItems.map((item) => item * currentMultiplier);
},
});
const unmount = computed.mount();
console.log(computed.state); // [2, 4, 6]
unmount();Derived from Derived
Derived values can depend on other Derived values, forming a computation graph.
const price = new Store(100);
const taxRate = new Store(0.08);
const tax = new Derived({
deps: [price, taxRate],
fn: ({ currDepVals }) => currDepVals[0] * currDepVals[1],
});
const total = new Derived({
deps: [price, tax],
fn: ({ currDepVals }) => currDepVals[0] + currDepVals[1],
});
const taxUnmount = tax.mount();
const totalUnmount = total.mount();
console.log(total.state); // 108
price.setState(() => 200);
console.log(total.state); // 216
totalUnmount();
taxUnmount();Subscriptions on Derived
Subscribe to derived state changes the same way as Store.
const count = new Store(0);
const doubled = new Derived({
deps: [count],
fn: ({ currDepVals }) => currDepVals[0] * 2,
});
const unmount = doubled.mount();
const unsubscribe = doubled.subscribe(() => {
console.log('Doubled is now:', doubled.state);
});
count.setState(() => 5);
// logs: "Doubled is now: 10"
unsubscribe();
unmount();Derived Lifecycle Hooks
Derived supports onSubscribe and onUpdate hooks, similar to Store.
const source = new Store(0);
const derived = new Derived({
deps: [source],
fn: ({ currDepVals }) => currDepVals[0] * 10,
onSubscribe: (_listener, _derived) => {
console.log('Derived subscriber added');
return () => console.log('Derived subscriber removed');
},
onUpdate: () => {
console.log('Derived recomputed');
},
});Batching Updates
batch groups multiple state updates so subscribers and derived values recompute only once.
import { Store, Derived, batch } from '@tanstack/store';
const a = new Store(1);
const b = new Store(2);
const sum = new Derived({
deps: [a, b],
fn: ({ currDepVals }) => currDepVals[0] + currDepVals[1],
});
const unmount = sum.mount();
let recomputeCount = 0;
sum.subscribe(() => recomputeCount++);
batch(() => {
a.setState(() => 10);
b.setState(() => 20);
});
console.log(sum.state); // 30
console.log(recomputeCount); // 1 (computed once, not twice)
unmount();Without batch, each setState triggers a separate recomputation and notification.
Effects
Effect runs a side-effect function when its dependencies change. Like Derived, it requires mounting.
import { Store, Effect } from '@tanstack/store';
const theme = new Store<'light' | 'dark'>('light');
const effect = new Effect({
deps: [theme],
fn: () => {
document.documentElement.dataset.theme = theme.state;
},
});
const unmount = effect.mount();
theme.setState(() => 'dark');
// document.documentElement.dataset.theme is now "dark"
unmount();Effects are useful for synchronizing store state with external systems (DOM, localStorage, analytics).
Eager Effects
By default, an Effect waits for the first dependency change before running. Set eager: true to run the effect immediately when mounted.
import { Store, Effect } from '@tanstack/store';
const count = new Store(0);
const effect = new Effect({
deps: [count],
fn: () => {
console.log('Count:', count.state);
},
eager: true,
});
const unmount = effect.mount();
// logs immediately: "Count: 0"
count.setState(() => 1);
// logs: "Count: 1"
unmount();Effect with Multiple Dependencies
const token = new Store<string | null>(null);
const baseUrl = new Store('https://api.example.com');
const effect = new Effect({
deps: [token, baseUrl],
fn: () => {
if (token.state) {
console.log(`Configured client: ${baseUrl.state} with auth`);
}
},
});
const unmount = effect.mount();
token.setState(() => 'abc123');
// logs: "Configured client: https://api.example.com with auth"
unmount();React Integration
Installation
npm install @tanstack/react-store@tanstack/react-store re-exports the core Store class, so a separate @tanstack/store install is not required.
useStore Hook
useStore subscribes a React component to a Store instance. The component re-renders when the store state changes.
import { Store, useStore } from '@tanstack/react-store';
const countStore = new Store(0);
function Counter() {
const count = useStore(countStore);
return (
<div>
<span>Count: {count}</span>
<button onClick={() => countStore.setState((prev) => prev + 1)}>
Increment
</button>
</div>
);
}Selectors
Pass a selector function as the second argument to subscribe to a slice of state. The component only re-renders when the selected value changes.
const appStore = new Store({
user: { name: 'Alice', role: 'admin' },
theme: 'dark',
notifications: 5,
});
function UserName() {
const name = useStore(appStore, (state) => state.user.name);
return <span>{name}</span>;
}
function NotificationBadge() {
const count = useStore(appStore, (state) => state.notifications);
return count > 0 ? <span>{count}</span> : null;
}Updating theme does not re-render UserName because the selector returns state.user.name, which has not changed.
Shallow Comparison
When a selector returns a new object or array reference on every call, use shallow to compare by value instead of reference.
import { useStore, shallow } from '@tanstack/react-store';
const todoStore = new Store({
items: [
{ id: 1, text: 'Buy milk', done: false },
{ id: 2, text: 'Walk dog', done: true },
],
});
function PendingTodos() {
const pending = useStore(
todoStore,
(state) => state.items.filter((item) => !item.done),
shallow,
);
return (
<ul>
{pending.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}Without shallow, the filter call creates a new array reference every time, causing re-renders even when the filtered result has not changed.
Store Outside Components
Define stores outside of React components. This avoids recreating the store on every render and enables sharing across the component tree.
import { Store, useStore } from '@tanstack/react-store';
export const store = new Store({
dogs: 0,
cats: 0,
});
function Display({ animal }: { animal: 'dogs' | 'cats' }) {
const count = useStore(store, (state) => state[animal]);
return (
<div>
{animal}: {count}
</div>
);
}
function Increment({ animal }: { animal: 'dogs' | 'cats' }) {
return (
<button
onClick={() =>
store.setState((prev) => ({
...prev,
[animal]: prev[animal] + 1,
}))
}
>
Add {animal}
</button>
);
}
function App() {
return (
<div>
<Increment animal="dogs" />
<Display animal="dogs" />
<Increment animal="cats" />
<Display animal="cats" />
</div>
);
}Derived Values in React
Use Derived with useStore to subscribe to computed values. Mount the Derived outside the component.
import { Store, Derived, useStore } from '@tanstack/react-store';
const cartStore = new Store({
items: [
{ name: 'Widget', price: 10, qty: 2 },
{ name: 'Gadget', price: 25, qty: 1 },
],
});
const totalDerived = new Derived({
deps: [cartStore],
fn: ({ currDepVals }) => {
const [cart] = currDepVals;
return cart.items.reduce((sum, item) => sum + item.price * item.qty, 0);
},
});
totalDerived.mount();
function CartTotal() {
const total = useStore(totalDerived);
return <div>Total: ${total}</div>;
}Updating State from Event Handlers
Call setState directly in event handlers. No dispatch or action creators needed.
const formStore = new Store({
email: '',
password: '',
});
function LoginForm() {
const email = useStore(formStore, (state) => state.email);
const password = useStore(formStore, (state) => state.password);
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log('Submit:', formStore.state);
}}
>
<input
type="email"
value={email}
onChange={(e) =>
formStore.setState((prev) => ({ ...prev, email: e.target.value }))
}
/>
<input
type="password"
value={password}
onChange={(e) =>
formStore.setState((prev) => ({ ...prev, password: e.target.value }))
}
/>
<button type="submit">Login</button>
</form>
);
}Performance Tips
- Use selectors: Always select the narrowest slice of state a component needs
- Use `shallow`: When selectors return derived objects or filtered arrays
- Avoid inline store creation: Instantiate stores outside components or in a
useStateinitializer - Batch related updates: Wrap multiple
setStatecalls inbatch()to prevent intermediate renders - Split stores: Prefer multiple small stores over one large store when state domains are unrelated