@tanstack/angular-table 9.0.0-alpha.47 → 9.0.0-alpha.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,396 @@
1
+ ---
2
+ name: angular/compose-with-tanstack-store
3
+ description: >
4
+ Compose TanStack Table v9 with `@tanstack/angular-store`. TanStack Table v9 is itself built on
5
+ TanStack Store — each state slice is an atom. Three read surfaces: `table.atoms.<slice>` (per-slice
6
+ readonly, signal-backed), `table.store` (flat readonly view), and `table.baseAtoms.<slice>`
7
+ (writable). The `atoms` table option lets you replace an internal slice with an external
8
+ TanStack Store atom for cross-app sharing (URL sync, persistence, multi-table coordination).
9
+ In Angular, native signals + `state` + `on[State]Change` is the default; reach for external
10
+ atoms only when ownership crosses an app boundary the signal model can't easily span.
11
+ type: composition
12
+ library: tanstack-table
13
+ framework: angular
14
+ library_version: '9.0.0-alpha.47'
15
+ requires:
16
+ - angular/table-state
17
+ - state-management
18
+ sources:
19
+ - TanStack/table:docs/framework/angular/guide/table-state.md
20
+ - TanStack/table:docs/framework/angular/angular-table.md
21
+ - TanStack/table:packages/angular-table/src/reactivity.ts
22
+ - TanStack/table:packages/angular-table/src/injectTable.ts
23
+ - TanStack/store:packages/angular-store/src/
24
+ ---
25
+
26
+ # Compose with TanStack Store (Angular)
27
+
28
+ > TanStack Table v9 **is** a TanStack Store consumer. The internal state model
29
+ > uses `alien-signals` atoms, exposed as `table.atoms.<slice>`,
30
+ > `table.baseAtoms.<slice>`, and the flat `table.store`. In Angular, those
31
+ > atoms are signal-backed via the `angularReactivity(injector)` binding.
32
+ >
33
+ > **For most Angular Table apps, native signals + `state` + `on[State]Change`
34
+ > is the right ownership model.** Reach for `@tanstack/angular-store` atoms
35
+ > when the slice must travel through code that doesn't share an injection
36
+ > scope with the table — URL sync, multi-table coordination, persistence
37
+ > layers, server caches, devtools.
38
+
39
+ ---
40
+
41
+ ## 1. The three read surfaces
42
+
43
+ Every TanStack Table instance exposes its state at three layers:
44
+
45
+ | Surface | Shape | Angular reactivity | Use when |
46
+ | ------------------------- | --------------------------------- | ------------------------------- | --------------------------------------------------- |
47
+ | `table.baseAtoms.<slice>` | Writable `Atom<T>` | Backed by an Angular `signal` | Direct internal writes; rare |
48
+ | `table.atoms.<slice>` | Readonly `Atom<T>` (derived) | Backed by an Angular `computed` | Reading or driving Angular reactivity per-slice |
49
+ | `table.store` | Readonly flat `Store<TableState>` | Backed by an Angular `computed` | Reading multiple slices in one go (devtools, debug) |
50
+
51
+ All three are populated only for **registered features** (`_features`). All
52
+ three are signal-backed via `angularReactivity(injector)`:
53
+ `createReadonlyAtom` → Angular `computed`, `createWritableAtom` → Angular
54
+ `signal`, subscriptions bridged through `toObservable(computed(signal), {
55
+ injector })`.
56
+
57
+ Read them inside templates, `computed(...)`, or `effect(...)` and Angular
58
+ tracks the dependency.
59
+
60
+ ```ts
61
+ this.table.atoms.pagination.get() // current value (reactive)
62
+ this.table.atoms.pagination.subscribe(obs) // RxJS observer form
63
+ this.table.store.state.pagination // flat snapshot read
64
+ this.table.baseAtoms.pagination.set(...) // direct internal write (avoid)
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 2. The `atoms` option — bring your own atom
70
+
71
+ The `atoms` table option lets you replace the internal `baseAtom` for a slice
72
+ with an **external TanStack Store atom**. Once registered, `table.atoms.<slice>`
73
+ reads from that external atom, and `table.set<X>` / `feature.on*Change` write
74
+ through it.
75
+
76
+ ```ts
77
+ import { Store } from '@tanstack/store'
78
+ import {
79
+ injectTable,
80
+ tableFeatures,
81
+ rowPaginationFeature,
82
+ rowSortingFeature,
83
+ type PaginationState,
84
+ type SortingState,
85
+ } from '@tanstack/angular-table'
86
+
87
+ const _features = tableFeatures({ rowPaginationFeature, rowSortingFeature })
88
+
89
+ // Module-scope (or app-scope) shared atoms
90
+ export const paginationStore = new Store<PaginationState>({
91
+ pageIndex: 0,
92
+ pageSize: 25,
93
+ })
94
+
95
+ export const sortingStore = new Store<SortingState>([])
96
+
97
+ @Component({...})
98
+ export class App {
99
+ readonly table = injectTable(() => ({
100
+ _features,
101
+ _rowModels: { /* … */ },
102
+ columns,
103
+ data: this.data(),
104
+ atoms: {
105
+ pagination: paginationStore,
106
+ sorting: sortingStore,
107
+ },
108
+ }))
109
+ }
110
+ ```
111
+
112
+ Now `paginationStore.state` and `table.atoms.pagination.get()` always agree.
113
+ Any other consumer of `paginationStore` (a URL-sync service, another table,
114
+ a devtools panel) sees the same updates.
115
+
116
+ > **External atoms take precedence over `state.<slice>`.** If you supply both
117
+ > `atoms.pagination` and `state.pagination`, the atom wins silently.
118
+
119
+ ---
120
+
121
+ ## 3. When to use external atoms vs Angular signals
122
+
123
+ The maintainer guidance: **Angular signals first**. Atoms when ownership
124
+ crosses boundaries.
125
+
126
+ | Scenario | Use |
127
+ | ------------------------------------------------------------ | ---------------------------------------------------------------------------- |
128
+ | Single component owns the slice | Angular `signal()` + `state` + `on[State]Change` |
129
+ | URL-sync (deep-linkable pagination, filter, sort) | External `Store` atom — see §4 |
130
+ | Two tables share a `globalFilter` | External `Store` atom on a shared module |
131
+ | Persisting to `localStorage` between sessions | External `Store` atom + subscribe to write |
132
+ | TanStack Query owns the server cache, table consumes filters | External `Store` atom or Angular signal — both work; pick what reads cleaner |
133
+ | Devtools / inspector across multiple tables | External `Store` atom — uniform consumer surface |
134
+
135
+ In a pure component-local scenario, an Angular signal is simpler — fewer
136
+ imports, no module-level globals, plays nicely with `OnPush`.
137
+
138
+ ---
139
+
140
+ ## 4. Real example — URL-synced pagination
141
+
142
+ ```ts
143
+ import { effect, signal } from '@angular/core'
144
+ import { Router, ActivatedRoute } from '@angular/router'
145
+ import { Store } from '@tanstack/store'
146
+ import {
147
+ injectTable,
148
+ tableFeatures,
149
+ rowPaginationFeature,
150
+ type PaginationState,
151
+ } from '@tanstack/angular-table'
152
+
153
+ const _features = tableFeatures({ rowPaginationFeature })
154
+
155
+ // Shared, app-scope atom
156
+ export const paginationStore = new Store<PaginationState>({
157
+ pageIndex: 0,
158
+ pageSize: 25,
159
+ })
160
+
161
+ @Component({
162
+ selector: 'page-route',
163
+ // …
164
+ })
165
+ export class PageRoute {
166
+ private readonly router = inject(Router)
167
+ private readonly route = inject(ActivatedRoute)
168
+
169
+ constructor() {
170
+ // Seed from URL once
171
+ const qp = this.route.snapshot.queryParamMap
172
+ paginationStore.setState((p) => ({
173
+ pageIndex: Number(qp.get('p') ?? p.pageIndex),
174
+ pageSize: Number(qp.get('s') ?? p.pageSize),
175
+ }))
176
+
177
+ // Mirror to URL
178
+ paginationStore.subscribe(() => {
179
+ const { pageIndex, pageSize } = paginationStore.state
180
+ this.router.navigate([], {
181
+ queryParams: { p: pageIndex, s: pageSize },
182
+ queryParamsHandling: 'merge',
183
+ replaceUrl: true,
184
+ })
185
+ })
186
+ }
187
+
188
+ readonly table = injectTable(() => ({
189
+ _features,
190
+ _rowModels: {
191
+ /* … */
192
+ },
193
+ columns,
194
+ data: this.data(),
195
+ atoms: {
196
+ pagination: paginationStore, // ← external atom owns the slice
197
+ },
198
+ }))
199
+ }
200
+ ```
201
+
202
+ `table.nextPage()` now writes to `paginationStore`, which writes to the URL.
203
+ A user copying the URL into a new tab lands on the same page.
204
+
205
+ ---
206
+
207
+ ## 5. Read external atom values reactively in Angular
208
+
209
+ `@tanstack/angular-store` provides `injectSelector` / `injectAtom` for
210
+ deriving Angular signals from TanStack Store atoms outside the table context:
211
+
212
+ ```ts
213
+ import { injectSelector } from '@tanstack/angular-store'
214
+ import { paginationStore } from './stores'
215
+
216
+ @Component({...})
217
+ export class StatsBar {
218
+ readonly pageIndex = injectSelector(paginationStore, (s) => s.pageIndex)
219
+ // Angular Signal<number>, signal-tracked normally
220
+ }
221
+ ```
222
+
223
+ Inside a table-owning component, you already have `table.atoms.pagination.get()`
224
+ — both forms are equivalent because the external atom _is_ the internal atom
225
+ for that slice.
226
+
227
+ ---
228
+
229
+ ## 6. Multi-table coordination
230
+
231
+ A common pattern: two tables on the same page should share a `globalFilter`.
232
+
233
+ ```ts
234
+ import { Store } from '@tanstack/store'
235
+
236
+ export const sharedFilter = new Store<string | null>(null)
237
+
238
+ // Table A
239
+ readonly tableA = injectTable(() => ({
240
+ _features: tableFeatures({ globalFilteringFeature }),
241
+ _rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
242
+ columns: columnsA,
243
+ data: this.dataA(),
244
+ atoms: { globalFilter: sharedFilter },
245
+ }))
246
+
247
+ // Table B (separate component, same module)
248
+ readonly tableB = injectTable(() => ({
249
+ _features: tableFeatures({ globalFilteringFeature }),
250
+ _rowModels: { filteredRowModel: createFilteredRowModel(filterFns) },
251
+ columns: columnsB,
252
+ data: this.dataB(),
253
+ atoms: { globalFilter: sharedFilter },
254
+ }))
255
+ ```
256
+
257
+ Calling `tableA.setGlobalFilter('foo')` updates `sharedFilter`, which Table B
258
+ also reads — both views filter together. Without the shared atom you'd need
259
+ a separate cross-component sync mechanism.
260
+
261
+ ---
262
+
263
+ ## 7. Reset semantics
264
+
265
+ This is a known sharp edge worth understanding:
266
+
267
+ - `table.resetPagination()` (and equivalents) updates _through the feature
268
+ state updater_. When the slice is owned by an external atom, the external
269
+ atom is updated.
270
+ - `table.reset()` (the core API) resets the **internal `baseAtoms`**. Do not
271
+ use it as the primary reset for externally-owned slices — it bypasses your
272
+ external atom.
273
+ - For an externally-owned slice, reset by writing to your atom directly
274
+ (`paginationStore.setState({ pageIndex: 0, pageSize: 25 })`) or by calling
275
+ the slice-specific reset API which routes through the updater.
276
+
277
+ ---
278
+
279
+ ## 8. State and atom precedence — the rules
280
+
281
+ For any given registered slice, the table picks state from this priority order:
282
+
283
+ 1. **`atoms.<slice>`** (external atom) — wins everything
284
+ 2. **`state.<slice>`** (controlled value, in initializer)
285
+ 3. **`initialState.<slice>`** (one-time seed)
286
+ 4. Feature default (slice's blank value)
287
+
288
+ **Don't supply more than one source for the same slice** unless you
289
+ intentionally want a specific layer to win. The precedence is silent — no
290
+ runtime warning today.
291
+
292
+ ---
293
+
294
+ ## 9. Cross-app patterns
295
+
296
+ ### Hydration / SSR-like state seeding
297
+
298
+ For SSR-rendered Angular tables that hydrate a known initial state from the
299
+ server payload, the atom approach is the cleanest:
300
+
301
+ ```ts
302
+ const paginationStore = new Store<PaginationState>(serverPayload.pagination)
303
+ // later:
304
+ atoms: {
305
+ pagination: paginationStore
306
+ }
307
+ ```
308
+
309
+ The atom is constructed with the hydrated value; the table never re-seeds
310
+ from `initialState` because `atoms` takes precedence.
311
+
312
+ ### Devtools / inspector
313
+
314
+ A separate devtools component can subscribe to `paginationStore`,
315
+ `sortingStore`, etc. without holding a reference to the table — useful when
316
+ the devtools live in a different injection scope.
317
+
318
+ ---
319
+
320
+ ## Failure modes
321
+
322
+ ### 1. (CRITICAL) Reimplementing TanStack Store with raw signals or Subjects
323
+
324
+ ```ts
325
+ // ❌ A homegrown shared store
326
+ @Injectable({ providedIn: 'root' })
327
+ export class FilterService {
328
+ readonly value = signal<string | null>(null)
329
+ setFilter(v: string | null) { this.value.set(v) }
330
+ }
331
+
332
+ // then trying to pipe it into the table…
333
+ state: { globalFilter: this.filterService.value() }
334
+ onGlobalFilterChange: (u) => /* ... */
335
+ ```
336
+
337
+ This works, but you've also lost the atom-bridge that lets `table.setGlobalFilter`
338
+ write back through. The atom flow (`atoms: { globalFilter: filterStore }`) is
339
+ fewer moving parts and idiomatic v9. Prefer it when the slice spans multiple
340
+ consumers.
341
+
342
+ ### 2. (CRITICAL) Supplying both `state.x` and `atoms.x` for the same slice
343
+
344
+ The atom wins; the Angular signal becomes a write-only ghost. No runtime
345
+ warning today. Pick one source of truth per slice. The most common bug here is
346
+ "I added atoms.pagination but the on[State]Change handler I left from before
347
+ no longer fires" — it does fire (the atom is updated through the table's
348
+ updater plumbing), but your Angular signal isn't being read by the table.
349
+
350
+ ### 3. (CRITICAL) Using `table.baseAtoms.x.set(...)` to update an externally-owned slice
351
+
352
+ `baseAtoms` are the internal writable atoms. When `atoms.x` is registered,
353
+ `table.atoms.x` and the feature updater route through the external atom, but
354
+ the internal `baseAtoms.x` is now an orphan — writing to it has no effect on
355
+ the table's behavior. Write to the external atom instead.
356
+
357
+ ### 4. (HIGH) Calling `table.reset()` on an externally-owned slice
358
+
359
+ `table.reset()` resets the internal `baseAtoms` — bypasses your external
360
+ atom. Use slice-specific resets (`resetSorting()`, `resetPagination()`,
361
+ `resetGlobalFilter()`) or write to the external atom directly.
362
+
363
+ ### 5. (HIGH) Forgetting that external atom state is reactive in Angular
364
+
365
+ Reading `paginationStore.state` in a template **is reactive** in v9 because
366
+ the adapter wraps it — but reading it in plain TypeScript outside a reactive
367
+ scope is a snapshot. Use `injectSelector(paginationStore, …)` to get an
368
+ Angular signal for general consumption, or read `table.atoms.pagination.get()`
369
+ inside the component that owns the table.
370
+
371
+ ### 6. (MEDIUM) Putting `new Store(...)` inside a component class field
372
+
373
+ Module-level (or `providedIn: 'root'` service) is the right place. Creating a
374
+ `new Store(...)` in a component field gives you a per-instance atom — which
375
+ defeats the cross-app sharing point. Use external atoms specifically for
376
+ _shared_ state.
377
+
378
+ ### 7. (MEDIUM) Hand-rolling subscription cleanup
379
+
380
+ When you `paginationStore.subscribe(fn)`, that returns an unsubscribe. Inside
381
+ an Angular component, prefer Angular's `effect(...) +
382
+ injectSelector(paginationStore, …)` for derived signals, or
383
+ `DestroyRef.onDestroy(unsubscribe)` for raw subscriptions.
384
+
385
+ ---
386
+
387
+ ## See also
388
+
389
+ - `tanstack-table/angular/table-state` — the prerequisite atom model
390
+ - `tanstack-table/angular/client-to-server` — server-driven tables (where
391
+ external atoms shine for URL sync)
392
+ - `tanstack-table/angular/compose-with-tanstack-query` — Query + Table
393
+ (sometimes external atoms simplify the bridge)
394
+ - `tanstack-table/core/state-management` — framework-agnostic atom semantics
395
+ - `@tanstack/angular-store` docs — `injectSelector`, `injectAtom`,
396
+ `createStoreContext`