TanStack Store provides Angular support through the @tanstack/angular-store package, which integrates with Angular’s modern signals-based reactivity system.
Installation
Install the Angular adapter package:
npm install @tanstack/angular-store
The @tanstack/angular-store package re-exports everything from @tanstack/store, so you only need to install the Angular package.
Angular Version Requirements
This package requires Angular 19+ as it uses the modern signals API including linkedSignal and Signal.
Basic Usage
The primary way to use TanStack Store in Angular is through the injectStore function:
import { Component } from '@angular/core'
import { createStore, injectStore } from '@tanstack/angular-store'
// Create a store
const counterStore = createStore({
count: 0,
})
@Component({
selector: 'app-counter',
standalone: true,
template: `
<div>
<p>Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
</div>
`,
})
export class CounterComponent {
// Inject the store - returns a Signal
count = injectStore(counterStore, (state) => state.count)
increment() {
counterStore.setState((prev) => ({ count: prev.count + 1 }))
}
}
The injectStore Function
The injectStore function subscribes your component to store updates and returns an Angular Signal that automatically updates.
Signature
function injectStore<TState, TSelected>(
store: Atom<TState> | ReadonlyAtom<TState>,
selector?: (state: TState) => TSelected,
options?: CreateSignalOptions<TSelected> & { injector?: Injector }
): Signal<TSelected>
Parameters
store - The store instance to subscribe to
selector - (Optional) A function that selects which part of the state you need. Defaults to returning the entire state
options.equal - (Optional) A custom equality function. Defaults to shallow
options.injector - (Optional) A custom injector. If not provided, must be called within an injection context
Return Value
Returns a readonly Angular Signal<TSelected>. Call it like count() to get the current value.
Injection Context
injectStore must be called within an injection context (constructor, field initializer, or factory function) unless you provide a custom injector.
import { Component, Injector, inject } from '@angular/core'
import { injectStore } from '@tanstack/angular-store'
@Component({
selector: 'app-example',
template: `<div>{{ value() }}</div>`,
})
export class ExampleComponent {
// ✅ Works - called in field initializer (injection context)
value = injectStore(myStore, (state) => state.value)
constructor() {
// ✅ Works - called in constructor (injection context)
const another = injectStore(myStore, (state) => state.other)
}
someMethod() {
// ❌ Error - not in injection context
// const value = injectStore(myStore, (state) => state.value)
// ✅ Works - provide injector explicitly
const injector = inject(Injector)
const value = injectStore(myStore, (state) => state.value, { injector })
}
}
Selector Optimization
The selector function allows you to subscribe to only the parts of state you need:
import { Component } from '@angular/core'
import { createStore, injectStore } from '@tanstack/angular-store'
const appStore = createStore({
user: { name: 'Alice', age: 30 },
settings: { theme: 'dark', notifications: true },
})
@Component({
selector: 'app-user-profile',
template: `<h1>Welcome, {{ userName() }}!</h1>`,
})
export class UserProfileComponent {
// Only updates when user.name changes
userName = injectStore(appStore, (state) => state.user.name)
}
@Component({
selector: 'app-settings',
template: `
<div>
<p>Theme: {{ settings().theme }}</p>
<p>Notifications: {{ settings().notifications ? 'On' : 'Off' }}</p>
</div>
`,
})
export class SettingsComponent {
// Only updates when settings change
settings = injectStore(appStore, (state) => state.settings)
}
Custom Equality Functions
For complex state selections, provide a custom equality function:
import { Component } from '@angular/core'
import { createStore, injectStore } from '@tanstack/angular-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)
}
@Component({
selector: 'app-todo-list',
template: `
<ul>
@for (todo of activeTodos(); track todo.id) {
<li>{{ todo.text }}</li>
}
</ul>
`,
})
export class TodoListComponent {
// Use deep equality for array comparison
activeTodos = injectStore(
todoStore,
(state) => state.todos.filter((todo) => !todo.completed),
{ equal: deepEqual }
)
}
Shallow Equality
The package uses shallow equality by default for object comparisons:
import { Component } from '@angular/core'
import { createStore, injectStore } from '@tanstack/angular-store'
const userStore = createStore({
profile: { name: 'Alice', email: '[email protected]' },
preferences: { theme: 'dark' },
})
@Component({
selector: 'app-user-card',
template: `
<div>
<h2>{{ profile().name }}</h2>
<p>{{ profile().email }}</p>
</div>
`,
})
export class UserCardComponent {
// Uses shallow equality by default
profile = injectStore(userStore, (state) => state.profile)
}
Complete Example: Todo App
Here’s a complete Todo application using Angular standalone components:
import { Component, signal } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { createStore, injectStore } from '@tanstack/angular-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 }))
}
@Component({
selector: 'app-todo',
standalone: true,
imports: [FormsModule],
template: `
<div>
<h1>Todo App</h1>
<form (ngSubmit)="handleSubmit()">
<input
[(ngModel)]="input"
name="todo"
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
<div>
<button (click)="setFilter('all')">All</button>
<button (click)="setFilter('active')">Active</button>
<button (click)="setFilter('completed')">Completed</button>
</div>
<ul>
@for (todo of todos(); track todo.id) {
<li>
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo.id)"
/>
<span [style.text-decoration]="todo.completed ? 'line-through' : 'none'">
{{ todo.text }}
</span>
</li>
}
</ul>
</div>
`,
})
export class TodoAppComponent {
input = ''
filter = injectStore(todoStore, (state) => state.filter)
todos = injectStore(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
})
handleSubmit() {
if (this.input.trim()) {
addTodo(this.input)
this.input = ''
}
}
toggleTodo(id: number) {
toggleTodo(id)
}
setFilter(filter: TodoState['filter']) {
setFilter(filter)
}
}
Angular-Specific Considerations
Signals Integration
The Angular adapter uses linkedSignal internally:
- Fully integrates with Angular’s signals API
- Works with
computed() and effect()
- Automatically handles cleanup with
DestroyRef
Working with Angular Signals
import { Component, computed, effect } from '@angular/core'
import { createStore, injectStore } from '@tanstack/angular-store'
const countStore = createStore({ count: 0, multiplier: 2 })
@Component({
selector: 'app-counter',
template: `<div>Result: {{ result() }}</div>`,
})
export class CounterComponent {
count = injectStore(countStore, (state) => state.count)
multiplier = injectStore(countStore, (state) => state.multiplier)
// Use with computed
result = computed(() => this.count() * this.multiplier())
constructor() {
// Use with effect
effect(() => {
console.log('Count changed:', this.count())
})
}
}
Dependency Injection
Create injectable services for your stores:
import { Injectable } from '@angular/core'
import { createStore } from '@tanstack/angular-store'
interface AppState {
user: { name: string; id: number } | null
isAuthenticated: boolean
}
@Injectable({ providedIn: 'root' })
export class AppStoreService {
private store = createStore<AppState>({
user: null,
isAuthenticated: false,
})
getStore() {
return this.store
}
login(user: { name: string; id: number }) {
this.store.setState({ user, isAuthenticated: true })
}
logout() {
this.store.setState({ user: null, isAuthenticated: false })
}
}
// Use in component
@Component({
selector: 'app-header',
template: `<div>{{ userName() }}</div>`,
})
export class HeaderComponent {
private appStore = inject(AppStoreService)
userName = injectStore(
this.appStore.getStore(),
(state) => state.user?.name ?? 'Guest'
)
}
Server-Side Rendering (SSR)
TanStack Store works with Angular Universal:
import { Injectable } from '@angular/core'
import { createStore } from '@tanstack/angular-store'
@Injectable({ providedIn: 'root' })
export class DataStoreService {
private store = createStore({ data: null, isLoading: false })
getStore() {
return this.store
}
initialize(initialData: any) {
this.store.setState({ data: initialData, isLoading: false })
}
}
- Use selective subscriptions: Subscribe only to the state you need
- Leverage signals: Combine with
computed() for derived state
- Injectable services: Wrap stores in services for better organization
- OnPush change detection: Works perfectly with signals-based stores
Derived Stores
Create derived stores that react to other stores:
import { Component } from '@angular/core'
import { createStore, injectStore } from '@tanstack/angular-store'
const countStore = createStore(0)
const doubleStore = createStore(() => ({ value: countStore.state * 2 }))
@Component({
selector: 'app-counter-display',
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">Increment</button>
</div>
`,
})
export class CounterDisplayComponent {
count = injectStore(countStore, (state) => state)
double = injectStore(doubleStore, (state) => state.value)
increment() {
countStore.setState((prev) => prev + 1)
}
}
API Reference
For detailed API documentation, see:
- See the API reference for detailed documentation:
TypeScript Support
The Angular adapter provides full TypeScript support:
import { Component } from '@angular/core'
import { createStore, injectStore } from '@tanstack/angular-store'
import type { Signal } from '@angular/core'
interface AppState {
user: { name: string; id: number }
isAuthenticated: boolean
}
const store = createStore<AppState>({
user: { name: '', id: 0 },
isAuthenticated: false,
})
@Component({
selector: 'app-component',
template: `<div>{{ userName() }}</div>`,
})
export class AppComponent {
// TypeScript infers the correct types
userName = injectStore(store, (state) => state.user.name)
// userName is Signal<string>
}