Welcome back, future TanStack master! So far, we’ve journeyed through powerful libraries like TanStack Query for managing server-side data, TanStack Table for displaying complex data, and TanStack Router for navigation. But what about the state that lives purely within your application’s client-side, the “local” state that doesn’t need to be fetched from a server? This is where TanStack Store shines!
In this chapter, we’ll dive deep into TanStack Store, a lightweight, flexible, and highly performant state management library. You’ll learn how to define, update, and selectively subscribe to your application’s local state, achieving fine-grained reactivity that keeps your UI snappy. We’ll explore its core principles, understand why it’s a perfect companion to TanStack Query, and get hands-on with practical examples. By the end, you’ll have a clear mental model for distinguishing between server-state and client-state and confidently managing both.
To get the most out of this chapter, you should have a basic understanding of component-level state management (like React’s useState or Vue’s ref) and a general grasp of how state changes can trigger UI updates. Let’s unlock the power of TanStack Store!
What is TanStack Store?
Imagine you have a light switch. When you flip it, the light immediately turns on or off. This action and its immediate effect on the light bulb is a lot like client-side state. It’s state that’s entirely managed within your application, without needing to talk to an external server. Examples include:
- Whether a modal dialog is open or closed.
- The current theme (light/dark) of your application.
- The value of an input field before it’s submitted to a server.
- A shopping cart’s items that haven’t been checked out yet.
TanStack Store is a headless, framework-agnostic library designed specifically for managing this kind of synchronous, client-side state. “Headless” means it provides the core logic for state management but doesn’t dictate how your UI looks or which framework you use. It offers adapters for popular frameworks like React, Vue, and Solid.
While TanStack Query excels at managing server-state (data that comes from an API, needs caching, invalidation, etc.), TanStack Store is its perfect partner for client-state. They address different, yet complementary, concerns.
Key Characteristics:
- Minimal Boilerplate: It’s designed to be simple and easy to get started with.
- Fine-Grained Reactivity: This is a superpower! It ensures that only the components that are actually using a specific piece of state get re-rendered when that piece of state changes. This prevents unnecessary UI updates and boosts performance.
- Immutable Updates: Like many modern state management solutions, TanStack Store encourages immutable updates, meaning you always create new state objects rather than directly modifying existing ones. This makes state changes predictable and easier to track.
- Inspired by Zustand: If you’re familiar with Zustand, you’ll find TanStack Store’s API very intuitive, as it draws heavily from Zustand’s design principles.
Core Concepts: Building Your Store
Let’s break down the fundamental building blocks of TanStack Store.
1. Defining a Store with createStore
The heart of TanStack Store is the createStore function. This is how you define your global (or semi-global) client state and the actions that can modify it.
import { createStore } from '@tanstack/store';
// This is where we define our store!
const myStore = createStore({
// initialState: This is the initial shape and values of your state.
initialState: {
message: "Hello from TanStack Store!",
count: 0,
isModalOpen: false,
},
// actions: These are functions that define how your state can be updated.
actions: {
// An action to update the message
setMessage: (state, newMessage: string) => ({
...state, // Always spread the existing state to maintain immutability
message: newMessage,
}),
// An action to increment the count
increment: (state) => ({
...state,
count: state.count + 1,
}),
// An action to toggle the modal
toggleModal: (state) => ({
...state,
isModalOpen: !state.isModalOpen,
}),
},
});
Explanation:
createStore: This function takes an object with two main properties:initialStateandactions.initialState: This object defines the initial structure and values of your store’s state. Think of it as the default values for all the pieces of data your store will manage.actions: This object contains functions that describe how your state can change.- Each action function receives the current state as its first argument.
- It then returns a new partial state object. TanStack Store will immutably merge this new partial state with the existing state. Notice how we use the spread operator (
...state) to ensure we’re always working with a new object, not mutating the original.
2. Accessing State and Dispatching Actions
Once you’ve defined your store, you need a way for your components to:
- Read the current state.
- Call the actions to update the state.
This is typically done using framework-specific adapters. For React, this means using a hook like useStore from @tanstack/react-store.
// Inside a React component
import { useStore } from '@tanstack/react-store';
import { myStore } from '../store/myStore'; // Assuming you defined myStore in this path
function MyComponent() {
// 1. Selecting specific state: This is the key to fine-grained reactivity!
const message = useStore(myStore, (state) => state.message);
const count = useStore(myStore, (state) => state.count);
const isModalOpen = useStore(myStore, (state) => state.isModalOpen);
return (
<div>
<p>Message: {message}</p>
<p>Count: {count}</p>
<p>Modal Open: {isModalOpen ? 'Yes' : 'No'}</p>
{/* 2. Dispatching actions */}
<button onClick={() => myStore.actions.increment()}>Increment Count</button>
<button onClick={() => myStore.actions.setMessage('New message!')}>Change Message</button>
<button onClick={() => myStore.actions.toggleModal()}>Toggle Modal</button>
</div>
);
}
Explanation:
useStore(myStore, (state) => state.message): This is where the magic of fine-grained reactivity happens!- The first argument is your store instance (
myStore). - The second argument is a selector function. This function receives the entire store state and returns only the specific piece of state that your component needs.
- Why is this important? If
state.messagechanges, only components that selectedstate.messagewill re-render. Components that selectedstate.countwill not re-render unlessstate.countitself changes. This is incredibly efficient!
- The first argument is your store instance (
myStore.actions.increment(): To update the state, you simply call the action directly on yourmyStore.actionsobject.
Mental Model: Client State vs. Server State
Let’s solidify the distinction between TanStack Query and TanStack Store with a simple diagram:
What to Observe/Learn from the Diagram:
- TanStack Store (Light Blue): Handles state that originates and lives entirely within your client application. It’s fast, synchronous, and directly impacts the UI’s appearance or interactive elements without needing a network request.
- TanStack Query (Light Green): Handles state that originates from a remote server. It’s asynchronous, involves network requests, caching strategies, and often deals with data that can be stale.
- Working Together: Your frontend application (ClientApp) uses both! You might use TanStack Store to manage the “loading” state of a form before submission, and then TanStack Query to send the form data to the server and manage the result.
Step-by-Step Implementation: A Simple Counter Application
Let’s build a basic counter application using TanStack Store in React.
1. Project Setup
If you don’t have a React project, quickly create one:
# Using Vite for a quick setup (as of Jan 2026)
npm create vite@latest my-store-app -- --template react-ts
cd my-store-app
npm install
2. Install TanStack Store
Next, install the core TanStack Store library and its React adapter.
npm install @tanstack/store@latest @tanstack/react-store@latest
# As of January 2026, these are typically v1.x.x
(Note: Always refer to the official documentation for the absolute latest versions, but v1 is the current stable release pattern for TanStack Store.)
3. Define Your Store
Create a new file src/store/counterStore.ts and define your counter state and actions.
// src/store/counterStore.ts
import { createStore } from '@tanstack/store';
interface CounterState {
count: number;
}
export const counterStore = createStore<CounterState>({
initialState: {
count: 0,
},
actions: {
increment: (state) => ({
// We return a new object with the updated count
...state,
count: state.count + 1,
}),
decrement: (state) => ({
...state,
count: state.count - 1,
}),
reset: () => ({
// To reset, we just return the initial state for 'count'
count: 0,
}),
},
});
Explanation:
- We define an
interface CounterStateto give our store’s state a clear type. This is good practice for type safety. createStore<CounterState>: We explicitly type our store.initialState: Sets the starting value ofcountto0.actions: We’ve defined three actions:increment,decrement, andreset. Each takes the currentstateand returns a new object with the updatedcount. Forreset, we just setcountback to0.
4. Create a Counter Component
Now, let’s create a React component that uses this store. Create src/components/CounterDisplay.tsx.
// src/components/CounterDisplay.tsx
import React from 'react';
import { useStore } from '@tanstack/react-store';
import { counterStore } from '../store/counterStore';
function CounterDisplay() {
// Use the useStore hook to select the 'count' from our store.
// This component will only re-render if 'count' changes.
const count = useStore(counterStore, (state) => state.count);
return (
<div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h3>Current Count: {count}</h3>
<div style={{ display: 'flex', gap: '10px', marginTop: '10px' }}>
<button onClick={() => counterStore.actions.increment()}>Increment</button>
<button onClick={() => counterStore.actions.decrement()}>Decrement</button>
<button onClick={() => counterStore.actions.reset()}>Reset</button>
</div>
</div>
);
}
export default CounterDisplay;
Explanation:
useStore(counterStore, (state) => state.count): This is the critical line. We’re telling React to subscribe this component tocounterStore, but only to thecountproperty. If any other part of the store were to change (which it won’t in this simple example, but imagine a more complex store), this component would not re-render.- The buttons directly call the actions defined in
counterStore.actionsto modify the state.
5. Integrate into Your App
Finally, open src/App.tsx and replace its content to display your CounterDisplay component.
// src/App.tsx
import React from 'react';
import CounterDisplay from './components/CounterDisplay';
import './App.css'; // Keep or remove default styling as you prefer
function App() {
return (
<div className="App" style={{ textAlign: 'center', fontFamily: 'Arial, sans-serif' }}>
<h1>TanStack Store Counter Example</h1>
<CounterDisplay />
<p style={{ marginTop: '20px', fontSize: '0.9em', color: '#666' }}>
Try clicking the buttons and observe the count update!
</p>
</div>
);
}
export default App;
Now, run your application:
npm run dev
Open your browser to the specified URL (usually http://localhost:5173). You should see your counter with increment, decrement, and reset buttons!
Mini-Challenge: Advanced Counter Features
You’ve built a basic counter. Now, let’s enhance it to solidify your understanding of adding new state and actions.
Challenge:
- Add a
stepproperty to yourcounterStore’s state. Initialize it to1. - Modify the
incrementanddecrementactions to use thisstepvalue. So,incrementshould addstate.steptostate.count, anddecrementshould subtractstate.step. - Add a new action
setStep(newStep: number)to your store that allows changing thestepvalue. - In your
CounterDisplaycomponent, add an input field that allows the user to change thestepvalue, and a button to apply it using your newsetStepaction. - Display the current
stepvalue in your component.
Hint:
Remember to add the new step property to your CounterState interface and to your initialState. For the input, you’ll need to manage its local state (e.g., using useState within the CounterDisplay component) before dispatching the setStep action to the global store.
What to observe/learn: This challenge will reinforce how to extend existing stores, add new actions, and integrate local component state with global store state for user input. You’ll see how multiple parts of the store can be selected and used independently.
Common Pitfalls & Troubleshooting
- Forgetting Selectors (or using
useStore(store)directly):- Pitfall: If you use
const state = useStore(myStore);without a selector function, your component will re-render every time any part ofmyStore’s state changes. This defeats the purpose of fine-grained reactivity and can lead to performance issues in complex applications. - Solution: Always provide a selector function to
useStore(e.g.,useStore(myStore, (s) => s.count)) to subscribe only to the parts of the state your component truly needs.
- Pitfall: If you use
- Direct State Mutation:
- Pitfall: Accidentally modifying the state object directly within an action, like
state.count++orstate.items.push(newItem). This breaks immutability, can lead to unexpected behavior, and prevents TanStack Store from detecting changes correctly. - Solution: Always return a new object (or array) with the updated values. Use the spread operator (
...state) to copy existing properties and then override the ones you want to change. For arrays, use methods likemap,filter,slice, or...to create new arrays.
// BAD: Mutates state directly // increment: (state) => { state.count++; return state; } // GOOD: Returns a new state object increment: (state) => ({ ...state, count: state.count + 1 }) - Pitfall: Accidentally modifying the state object directly within an action, like
- Confusing TanStack Store with TanStack Query:
- Pitfall: Trying to use TanStack Store for data that needs caching, background re-fetching, invalidation, or optimistic updates (i.e., server-state concerns).
- Solution: Remember the mental model:
- TanStack Query: For asynchronous, server-derived data. Think API calls, database records, data that might go stale.
- TanStack Store: For synchronous, client-specific UI state. Think modals, themes, form input values (before submission), local settings.
- They work best together, each handling its appropriate domain.
Summary
Congratulations! You’ve successfully navigated the world of TanStack Store and now understand how to manage client-side state with fine-grained reactivity.
Here are the key takeaways from this chapter:
- TanStack Store is a headless, framework-agnostic library for managing client-side, local state.
- It perfectly complements TanStack Query, which focuses on server-side state.
- You define your state and its update logic using the
createStorefunction, providing aninitialStateandactions. - Actions receive the current state and return a new partial state object to ensure immutability.
- Framework adapters (like
useStorefor React) allow components to selectively subscribe to parts of the state, enabling fine-grained reactivity and preventing unnecessary re-renders. - Always use selector functions with
useStoreto optimize performance. - Avoid direct state mutation; always return new objects or arrays when updating state.
You now have a powerful tool in your TanStack arsenal for managing all forms of application state. In the next chapter, we’ll likely explore how these powerful libraries can be combined to build even more robust and performant applications, perhaps diving into TanStack Form or TanStack Router integration with state. Keep up the great work!
References
- TanStack Store Official Documentation
- TanStack React Store Official Documentation
- TanStack Query Official Documentation (Distinction between client and server state)
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.