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: initialState and actions.
  • 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:

  1. Read the current state.
  2. 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.message changes, only components that selected state.message will re-render. Components that selected state.count will not re-render unless state.count itself changes. This is incredibly efficient!
  • myStore.actions.increment(): To update the state, you simply call the action directly on your myStore.actions object.

Mental Model: Client State vs. Server State

Let’s solidify the distinction between TanStack Query and TanStack Store with a simple diagram:

graph TD User --> Interaction[Interacts with UI] ClientApp[Frontend Application] Interaction --> NeedLocalState[Needs local UI state] TanStackStore[TanStack Store] NeedLocalState --> ManageUIState[Manages UI State] UIState[UI State] ManageUIState --> UpdateUI[Updates UI directly] ClientApp --> NeedRemoteData[Needs remote data] TanStackQuery[TanStack Query] NeedRemoteData --> FetchCacheSync[Fetches Caches Synchronizes] ServerAPI[Server API] FetchCacheSync --> RespondWithData[Responds with data] RespondWithData --> ProvideData[Provides data to UI] ProvideData --> ClientApp subgraph ClientSide["Client Side"] ClientApp TanStackStore UIState end subgraph ServerSide["Server Side"] ServerAPI end style TanStackStore fill:#ADD8E6,stroke:#333,stroke-width:2px style TanStackQuery fill:#90EE90,stroke:#333,stroke-width:2px

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 CounterState to 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 of count to 0.
  • actions: We’ve defined three actions: increment, decrement, and reset. Each takes the current state and returns a new object with the updated count. For reset, we just set count back to 0.

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 to counterStore, but only to the count property. 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.actions to 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:

  1. Add a step property to your counterStore’s state. Initialize it to 1.
  2. Modify the increment and decrement actions to use this step value. So, increment should add state.step to state.count, and decrement should subtract state.step.
  3. Add a new action setStep(newStep: number) to your store that allows changing the step value.
  4. In your CounterDisplay component, add an input field that allows the user to change the step value, and a button to apply it using your new setStep action.
  5. Display the current step value 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

  1. 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 of myStore’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.
  2. Direct State Mutation:
    • Pitfall: Accidentally modifying the state object directly within an action, like state.count++ or state.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 like map, 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 })
    
  3. 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 createStore function, providing an initialState and actions.
  • Actions receive the current state and return a new partial state object to ensure immutability.
  • Framework adapters (like useStore for 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 useStore to 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


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.