TanStack Store provides first-class React support through the @tanstack/react-store package, which uses React’s useSyncExternalStore hook for optimal performance and automatic re-rendering.
Installation
Install the React adapter package:
npm install @tanstack/react-store
The @tanstack/react-store package re-exports everything from @tanstack/store, so you only need to install the React package.
Basic Usage
The primary way to use TanStack Store in React is through the useStore hook:
import { createStore, useStore } from '@tanstack/react-store'
// Create a store
const counterStore = createStore({
count: 0,
})
function Counter() {
// Subscribe to the entire store state
const count = useStore(counterStore, (state) => state.count)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => counterStore.setState((prev) => ({ count: prev.count + 1 }))}>
Increment
</button>
</div>
)
}
The useStore Hook
The useStore hook subscribes your component to store updates and automatically triggers re-renders when selected state changes.
Signature
function useStore<TState, TSelected>(
store: Atom<TState> | ReadonlyAtom<TState>,
selector: (state: TState) => TSelected,
compare?: (a: TSelected, b: TSelected) => boolean
): TSelected
Parameters
store - The store instance to subscribe to
selector - A function that selects which part of the state you need
compare - (Optional) A custom equality function to determine if the selected value has changed. Defaults to Object.is
Selector Optimization
The selector function allows you to subscribe to only the parts of state you need. Your component will only re-render when the selected value changes:
import { createStore, useStore } from '@tanstack/react-store'
const appStore = createStore({
user: { name: 'Alice', age: 30 },
settings: { theme: 'dark', notifications: true },
})
function UserProfile() {
// Only re-renders when user.name changes
const userName = useStore(appStore, (state) => state.user.name)
return <h1>Welcome, {userName}!</h1>
}
function SettingsPanel() {
// Only re-renders when settings change
const settings = useStore(appStore, (state) => state.settings)
return (
<div>
<p>Theme: {settings.theme}</p>
<p>Notifications: {settings.notifications ? 'On' : 'Off'}</p>
</div>
)
}
Custom Equality Functions
For complex state selections, you can provide a custom equality function to prevent unnecessary re-renders:
import { createStore, useStore } from '@tanstack/react-store'
const todoStore = createStore({
todos: [
{ id: 1, text: 'Buy groceries', completed: false },
{ id: 2, text: 'Walk the dog', completed: true },
],
})
function deepEqual<T>(a: T, b: T): boolean {
return JSON.stringify(a) === JSON.stringify(b)
}
function TodoList() {
// Use deep equality to compare arrays
const activeTodos = useStore(
todoStore,
(state) => state.todos.filter((todo) => !todo.completed),
deepEqual
)
return (
<ul>
{activeTodos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
)
}
Shallow Equality
The package exports a shallow utility for shallow object comparison:
import { createStore, useStore, shallow } from '@tanstack/react-store'
const userStore = createStore({
profile: { name: 'Alice', email: '[email protected]' },
preferences: { theme: 'dark' },
})
function UserCard() {
// Only re-renders if profile properties change (not the object reference)
const profile = useStore(userStore, (state) => state.profile, shallow)
return (
<div>
<h2>{profile.name}</h2>
<p>{profile.email}</p>
</div>
)
}
The shallow function works with:
- Plain objects
- Maps
- Sets
- Dates
- Symbol keys
Complete Example: Todo App
Here’s a complete example demonstrating store creation, updates, and React integration:
import { createStore, useStore } from '@tanstack/react-store'
import { useState } from 'react'
interface Todo {
id: number
text: string
completed: boolean
}
interface TodoState {
todos: Todo[]
filter: 'all' | 'active' | 'completed'
}
const todoStore = createStore<TodoState>({
todos: [],
filter: 'all',
})
// Actions
const addTodo = (text: string) => {
todoStore.setState((state) => ({
...state,
todos: [
...state.todos,
{ id: Date.now(), text, completed: false },
],
}))
}
const toggleTodo = (id: number) => {
todoStore.setState((state) => ({
...state,
todos: state.todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}))
}
const setFilter = (filter: TodoState['filter']) => {
todoStore.setState((state) => ({ ...state, filter }))
}
function TodoApp() {
const [input, setInput] = useState('')
const filter = useStore(todoStore, (state) => state.filter)
const todos = useStore(todoStore, (state) => {
const { todos, filter } = state
if (filter === 'active') return todos.filter((t) => !t.completed)
if (filter === 'completed') return todos.filter((t) => t.completed)
return todos
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (input.trim()) {
addTodo(input)
setInput('')
}
}
return (
<div>
<h1>Todo App</h1>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
</li>
))}
</ul>
</div>
)
}
export default TodoApp
React-Specific Considerations
Concurrent Features
The React adapter uses useSyncExternalStore under the hood, which means it’s fully compatible with React 18’s concurrent features including:
- Concurrent rendering
- Automatic batching
- Transitions
- Suspense (when combined with proper error boundaries)
Server-Side Rendering (SSR)
TanStack Store works seamlessly with SSR. Create stores outside of components and initialize them with your server-side data:
import { createStore } from '@tanstack/react-store'
// Create the store at module level
const appStore = createStore({
data: null,
isLoading: false,
})
// Initialize with SSR data
export function initializeStore(initialData: any) {
appStore.setState({ data: initialData, isLoading: false })
}
- Use selectors wisely: Select only what you need to minimize re-renders
- Leverage equality functions: Use
shallow or custom comparisons for complex objects
- Memoize selectors: For expensive computations, consider memoizing your selector functions
- Create multiple stores: Instead of one large store, create multiple smaller stores for different domains
Derived Stores
You can create derived stores that automatically update based on other stores:
import { createStore, useStore } from '@tanstack/react-store'
const countStore = createStore(0)
const doubleStore = createStore(() => ({ value: countStore.state * 2 }))
function CounterDisplay() {
const count = useStore(countStore, (state) => state)
const double = useStore(doubleStore, (state) => state.value)
return (
<div>
<p>Count: {count}</p>
<p>Double: {double}</p>
<button onClick={() => countStore.setState((prev) => prev + 1)}>
Increment
</button>
</div>
)
}
API Reference
For detailed API documentation, see:
- useStore - React hook for subscribing to stores
- shallow - Shallow equality comparison utility
- createStore - Core store creation API
TypeScript Support
The React adapter provides full TypeScript support with automatic type inference:
import { createStore, useStore } from '@tanstack/react-store'
interface AppState {
user: { name: string; id: number }
isAuthenticated: boolean
}
const store = createStore<AppState>({
user: { name: '', id: 0 },
isAuthenticated: false,
})
function Component() {
// TypeScript knows the exact shape of state
const userName = useStore(store, (state) => state.user.name)
// userName is typed as string
return <div>{userName}</div>
}