TanStack Store provides Preact support through the @tanstack/preact-store package, which implements a lightweight version of React’s useSyncExternalStore for optimal performance.
Installation
Install the Preact adapter package:
npm install @tanstack/preact-store
The @tanstack/preact-store package re-exports everything from @tanstack/store, so you only need to install the Preact package.
Basic Usage
The primary way to use TanStack Store in Preact is through the useStore hook:
import { createStore, useStore } from '@tanstack/preact-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,
options?: { equal?: (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
options.equal - (Optional) A custom equality function. Defaults to shallow
Implementation Details
The Preact adapter includes a custom implementation of useSyncExternalStore that:
- Avoids importing
preact/compat (no side effects)
- Uses
useLayoutEffect and useEffect for optimal timing
- Provides the same guarantees as React’s
useSyncExternalStore
Selector Optimization
The selector function allows you to subscribe to only the parts of state you need:
import { createStore, useStore } from '@tanstack/preact-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:
import { createStore, useStore } from '@tanstack/preact-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),
{ equal: 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 (this is the default):
import { createStore, useStore, shallow } from '@tanstack/preact-store'
const userStore = createStore({
profile: { name: 'Alice', email: '[email protected]' },
preferences: { theme: 'dark' },
})
function UserCard() {
// Uses shallow comparison by default
const profile = useStore(userStore, (state) => state.profile, { equal: 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 Preact integration:
import { h } from 'preact'
import { useState } from 'preact/hooks'
import { createStore, useStore } from '@tanstack/preact-store'
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: Event) => {
e.preventDefault()
if (input.trim()) {
addTodo(input)
setInput('')
}
}
return (
<div>
<h1>Todo App</h1>
<form onSubmit={handleSubmit}>
<input
value={input}
onInput={(e) => setInput((e.target as HTMLInputElement).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
Preact-Specific Considerations
No preact/compat Required
Unlike some libraries, @tanstack/preact-store doesn’t require preact/compat. It includes a custom implementation of useSyncExternalStore specifically for Preact.
Lightweight Implementation
The adapter is highly optimized:
- Small bundle size
- No external dependencies (except Preact itself)
- Efficient re-render behavior
Server-Side Rendering (SSR)
TanStack Store works with Preact SSR. Create stores outside of components and initialize them with server data:
import { createStore } from '@tanstack/preact-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 shallow equality: The default works well for most cases
- Memoize selectors: For expensive computations, consider memoizing selector functions
- Create multiple stores: Instead of one large store, create multiple smaller stores
Derived Stores
You can create derived stores that automatically update based on other stores:
import { createStore, useStore } from '@tanstack/preact-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>
)
}
Migration from React
If you’re migrating from React, the API is identical:
// React
import { createStore, useStore } from '@tanstack/react-store'
// Preact - same API!
import { createStore, useStore } from '@tanstack/preact-store'
The only difference is the import path. Your component code can remain the same.
Comparison with Preact Signals
Preact has built-in signals. Here’s when to use TanStack Store:
Use TanStack Store when:
- You want a consistent API across multiple frameworks
- You need advanced features like derived stores
- You’re migrating from React
- You want structured state management
Use Preact Signals when:
- You want the lightest possible solution
- You’re building a Preact-only app
- You prefer a more reactive programming style
You can also use both together!
API Reference
For detailed API documentation, see:
- See the API reference for detailed documentation:
TypeScript Support
The Preact adapter provides full TypeScript support with automatic type inference:
import { createStore, useStore } from '@tanstack/preact-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>
}