@tanstack/angular-table 9.0.0-alpha.8 → 9.0.0-beta.1
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.
- package/README.md +127 -0
- package/dist/README.md +127 -0
- package/dist/fesm2022/tanstack-angular-table-static-functions.mjs +6 -0
- package/dist/fesm2022/tanstack-angular-table-static-functions.mjs.map +1 -0
- package/dist/fesm2022/tanstack-angular-table.mjs +1337 -242
- package/dist/fesm2022/tanstack-angular-table.mjs.map +1 -1
- package/dist/types/tanstack-angular-table-static-functions.d.ts +1 -0
- package/dist/types/tanstack-angular-table.d.ts +793 -0
- package/package.json +39 -19
- package/skills/angular/angular-rendering-directives/SKILL.md +415 -0
- package/skills/angular/angular-rendering-directives/references/content-shapes.md +142 -0
- package/skills/angular/angular-rendering-directives/references/create-table-hook-registries.md +89 -0
- package/skills/angular/angular-rendering-directives/references/di-tokens.md +171 -0
- package/skills/angular/angular-rendering-directives/references/flex-render-component-options.md +64 -0
- package/skills/angular/client-to-server/SKILL.md +467 -0
- package/skills/angular/compose-with-tanstack-query/SKILL.md +482 -0
- package/skills/angular/compose-with-tanstack-store/SKILL.md +397 -0
- package/skills/angular/compose-with-tanstack-virtual/SKILL.md +400 -0
- package/skills/angular/getting-started/SKILL.md +496 -0
- package/skills/angular/getting-started/references/feature-row-model-mapping.md +48 -0
- package/skills/angular/migrate-v8-to-v9/SKILL.md +419 -0
- package/skills/angular/migrate-v8-to-v9/references/v8-to-v9-mapping.md +261 -0
- package/skills/angular/production-readiness/SKILL.md +469 -0
- package/skills/angular/table-state/SKILL.md +429 -0
- package/skills/angular/table-state/references/external-atoms-and-app-hook.md +152 -0
- package/src/flex-render/context.ts +14 -0
- package/src/flex-render/flags.ts +34 -0
- package/src/flex-render/flexRenderComponent.ts +288 -0
- package/src/flex-render/flexRenderComponentFactory.ts +251 -0
- package/src/flex-render/renderer.ts +393 -0
- package/src/flex-render/view.ts +226 -0
- package/src/flexRender.ts +124 -0
- package/src/helpers/cell.ts +104 -0
- package/src/helpers/createTableHook.ts +499 -0
- package/src/helpers/flexRenderCell.ts +136 -0
- package/src/helpers/header.ts +99 -0
- package/src/helpers/table.ts +85 -0
- package/src/index.ts +23 -70
- package/src/injectTable.ts +212 -0
- package/src/{lazy-signal-initializer.ts → lazySignalInitializer.ts} +2 -2
- package/src/reactivity.ts +105 -0
- package/static-functions/index.ts +1 -0
- package/static-functions/ng-package.json +6 -0
- package/dist/esm2022/flex-render.mjs +0 -150
- package/dist/esm2022/index.mjs +0 -48
- package/dist/esm2022/lazy-signal-initializer.mjs +0 -43
- package/dist/esm2022/proxy.mjs +0 -83
- package/dist/esm2022/tanstack-angular-table.mjs +0 -5
- package/dist/flex-render.d.ts +0 -31
- package/dist/index.d.ts +0 -5
- package/dist/lazy-signal-initializer.d.ts +0 -5
- package/dist/proxy.d.ts +0 -3
- package/src/flex-render.ts +0 -187
- package/src/proxy.ts +0 -97
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: angular/compose-with-tanstack-query
|
|
3
|
+
description: >
|
|
4
|
+
Compose TanStack Table v9 with `@tanstack/angular-query-experimental` for server-side data.
|
|
5
|
+
Key the query on the controlled table state that drives the request (pagination, sorting,
|
|
6
|
+
filters); use `placeholderData: keepPreviousData` to avoid a "0 rows flash" between pages;
|
|
7
|
+
set `manualPagination` / `manualSorting` / `manualFiltering` for the slices the server owns;
|
|
8
|
+
drop the matching client `rowModels` factories; pass `rowCount` from the server response;
|
|
9
|
+
set `getRowId` for stable selection across refetches; hoist controlled slices to Angular
|
|
10
|
+
signals + `state` + `on[State]Change`. Alternative — `rxResource` / `httpResource` if you
|
|
11
|
+
don't want to add the Query dependency (see `client-to-server`).
|
|
12
|
+
type: composition
|
|
13
|
+
library: tanstack-table
|
|
14
|
+
framework: angular
|
|
15
|
+
library_version: '9.0.0-alpha.48'
|
|
16
|
+
requires:
|
|
17
|
+
- angular/table-state
|
|
18
|
+
- angular/client-to-server
|
|
19
|
+
- angular/getting-started
|
|
20
|
+
sources:
|
|
21
|
+
- TanStack/table:docs/framework/angular/guide/table-state.md
|
|
22
|
+
- TanStack/table:examples/angular/remote-data/
|
|
23
|
+
- TanStack/query:packages/angular-query-experimental/src/
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Compose with TanStack Query (Angular)
|
|
27
|
+
|
|
28
|
+
> Goal: server-driven Angular Table v9 with `@tanstack/angular-query-experimental`
|
|
29
|
+
> as the fetch / cache / refetch layer. The pattern is the same as the
|
|
30
|
+
> `examples/angular/remote-data/` example, just with `injectQuery` instead of
|
|
31
|
+
> `rxResource`.
|
|
32
|
+
>
|
|
33
|
+
> The non-Query variant (`rxResource` / `httpResource`) is documented in
|
|
34
|
+
> `tanstack-table/angular/client-to-server`. Both work — Query adds caching,
|
|
35
|
+
> request deduplication, background refetch, and offline coordination.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add @tanstack/angular-query-experimental
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then in `app.config.ts`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import {
|
|
49
|
+
provideTanStackQuery,
|
|
50
|
+
QueryClient,
|
|
51
|
+
} from '@tanstack/angular-query-experimental'
|
|
52
|
+
|
|
53
|
+
export const appConfig: ApplicationConfig = {
|
|
54
|
+
providers: [
|
|
55
|
+
provideTanStackQuery(new QueryClient()),
|
|
56
|
+
// ...
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 2. The pattern in one snippet
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import {
|
|
67
|
+
ChangeDetectionStrategy,
|
|
68
|
+
Component,
|
|
69
|
+
computed,
|
|
70
|
+
inject,
|
|
71
|
+
signal,
|
|
72
|
+
} from '@angular/core'
|
|
73
|
+
import { HttpClient, HttpParams } from '@angular/common/http'
|
|
74
|
+
import { lastValueFrom, map } from 'rxjs'
|
|
75
|
+
import {
|
|
76
|
+
injectQuery,
|
|
77
|
+
keepPreviousData,
|
|
78
|
+
} from '@tanstack/angular-query-experimental'
|
|
79
|
+
import {
|
|
80
|
+
FlexRender,
|
|
81
|
+
injectTable,
|
|
82
|
+
tableFeatures,
|
|
83
|
+
rowPaginationFeature,
|
|
84
|
+
rowSortingFeature,
|
|
85
|
+
globalFilteringFeature,
|
|
86
|
+
createColumnHelper,
|
|
87
|
+
type ColumnDef,
|
|
88
|
+
type PaginationState,
|
|
89
|
+
type SortingState,
|
|
90
|
+
} from '@tanstack/angular-table'
|
|
91
|
+
|
|
92
|
+
const features = tableFeatures({
|
|
93
|
+
rowPaginationFeature,
|
|
94
|
+
rowSortingFeature,
|
|
95
|
+
globalFilteringFeature,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const columnHelper = createColumnHelper<typeof features, Todo>()
|
|
99
|
+
|
|
100
|
+
type TodoResponse = { items: Array<Todo>; totalCount: number }
|
|
101
|
+
|
|
102
|
+
@Component({
|
|
103
|
+
selector: 'app-root',
|
|
104
|
+
imports: [FlexRender],
|
|
105
|
+
templateUrl: './app.html',
|
|
106
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
107
|
+
})
|
|
108
|
+
export class App {
|
|
109
|
+
private readonly http = inject(HttpClient)
|
|
110
|
+
|
|
111
|
+
// 1. Hoist controlled slices to Angular signals
|
|
112
|
+
readonly pagination = signal<PaginationState>({ pageIndex: 0, pageSize: 10 })
|
|
113
|
+
readonly sorting = signal<SortingState>([])
|
|
114
|
+
readonly globalFilter = signal<string | null>(null)
|
|
115
|
+
|
|
116
|
+
// 2. Query keyed on those signals — refetches when any of them change
|
|
117
|
+
readonly todosQuery = injectQuery(() => ({
|
|
118
|
+
queryKey: ['todos', this.pagination(), this.sorting(), this.globalFilter()],
|
|
119
|
+
queryFn: () => {
|
|
120
|
+
const p = this.pagination()
|
|
121
|
+
const s = this.sorting()
|
|
122
|
+
const f = this.globalFilter()
|
|
123
|
+
|
|
124
|
+
let params = new HttpParams({
|
|
125
|
+
fromObject: { _page: p.pageIndex + 1, _limit: p.pageSize },
|
|
126
|
+
})
|
|
127
|
+
if (f) params = params.set('title_like', f)
|
|
128
|
+
if (s.length) {
|
|
129
|
+
params = params
|
|
130
|
+
.set('_sort', s.map((x) => x.id).join(','))
|
|
131
|
+
.set('_order', s.map((x) => (x.desc ? 'desc' : 'asc')).join(','))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return lastValueFrom(
|
|
135
|
+
this.http
|
|
136
|
+
.get<Array<Todo>>('https://jsonplaceholder.typicode.com/todos', {
|
|
137
|
+
params,
|
|
138
|
+
observe: 'response',
|
|
139
|
+
})
|
|
140
|
+
.pipe(
|
|
141
|
+
map(
|
|
142
|
+
(res) =>
|
|
143
|
+
({
|
|
144
|
+
items: res.body ?? [],
|
|
145
|
+
totalCount: Number(res.headers.get('X-Total-Count')),
|
|
146
|
+
}) satisfies TodoResponse,
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
},
|
|
151
|
+
placeholderData: keepPreviousData, // ← prevents "0 rows" flash on refetch
|
|
152
|
+
staleTime: 30_000,
|
|
153
|
+
}))
|
|
154
|
+
|
|
155
|
+
// 3. Stable column defs (module-scope-able)
|
|
156
|
+
readonly columns: Array<ColumnDef<typeof features, Todo>> = [
|
|
157
|
+
columnHelper.accessor('id', { header: 'Id', cell: (i) => i.getValue() }),
|
|
158
|
+
columnHelper.accessor('title', {
|
|
159
|
+
header: 'Title',
|
|
160
|
+
cell: (i) => i.getValue(),
|
|
161
|
+
}),
|
|
162
|
+
columnHelper.accessor('completed', {
|
|
163
|
+
header: 'Completed',
|
|
164
|
+
cell: (i) => (i.getValue() ? '✅' : '❌'),
|
|
165
|
+
}),
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
// 4. Wire the table — manual flags, no client row models, rowCount, getRowId
|
|
169
|
+
readonly table = injectTable(() => {
|
|
170
|
+
const data = this.todosQuery.data() ?? { items: [], totalCount: 0 }
|
|
171
|
+
return {
|
|
172
|
+
features,
|
|
173
|
+
rowModels: {}, // ← dropped paginatedRowModel / sortedRowModel / filteredRowModel
|
|
174
|
+
columns: this.columns,
|
|
175
|
+
data: data.items,
|
|
176
|
+
getRowId: (row) => String(row.id),
|
|
177
|
+
|
|
178
|
+
state: {
|
|
179
|
+
pagination: this.pagination(),
|
|
180
|
+
sorting: this.sorting(),
|
|
181
|
+
globalFilter: this.globalFilter(),
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
manualPagination: true,
|
|
185
|
+
manualSorting: true,
|
|
186
|
+
manualFiltering: true,
|
|
187
|
+
|
|
188
|
+
rowCount: data.totalCount, // for getPageCount() under manualPagination
|
|
189
|
+
|
|
190
|
+
autoResetPageIndex: false, // we manage page resets explicitly
|
|
191
|
+
|
|
192
|
+
onPaginationChange: (u) =>
|
|
193
|
+
typeof u === 'function'
|
|
194
|
+
? this.pagination.update(u)
|
|
195
|
+
: this.pagination.set(u),
|
|
196
|
+
onSortingChange: (u) =>
|
|
197
|
+
typeof u === 'function' ? this.sorting.update(u) : this.sorting.set(u),
|
|
198
|
+
onGlobalFilterChange: (u) => {
|
|
199
|
+
typeof u === 'function'
|
|
200
|
+
? this.globalFilter.update(u)
|
|
201
|
+
: this.globalFilter.set(u)
|
|
202
|
+
this.pagination.update((p) => ({ ...p, pageIndex: 0 })) // UX reset
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 3. The four mandatory pieces
|
|
212
|
+
|
|
213
|
+
For server-driven Table + Query to work correctly:
|
|
214
|
+
|
|
215
|
+
1. **`queryKey` includes every signal the request reads.** If `pagination`
|
|
216
|
+
changes but `queryKey` doesn't include `this.pagination()`, the query
|
|
217
|
+
won't refetch.
|
|
218
|
+
2. **`placeholderData: keepPreviousData`** keeps the last response visible
|
|
219
|
+
during refetches. Without it, `todosQuery.data()` becomes `undefined`
|
|
220
|
+
mid-fetch, your table shows 0 rows for a frame, the user notices.
|
|
221
|
+
3. **`manualPagination` / `manualSorting` / `manualFiltering: true`** for
|
|
222
|
+
slices the server owns + **drop the matching `rowModels` factories** so
|
|
223
|
+
the table doesn't re-process the data the server already filtered/sorted/paged.
|
|
224
|
+
4. **`rowCount: data.totalCount`** (or `pageCount`) so `getPageCount()`
|
|
225
|
+
computes correctly under `manualPagination`.
|
|
226
|
+
|
|
227
|
+
Plus: **`getRowId` for stable identity** across refetches (required for
|
|
228
|
+
correct row selection).
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## 4. Loading and error UI
|
|
233
|
+
|
|
234
|
+
`injectQuery` returns a signal-rich object. Read the state in templates:
|
|
235
|
+
|
|
236
|
+
```html
|
|
237
|
+
@if (todosQuery.isPending()) {
|
|
238
|
+
<p>Loading…</p>
|
|
239
|
+
} @else if (todosQuery.isError()) {
|
|
240
|
+
<p>Failed: {{ todosQuery.error()?.message }}</p>
|
|
241
|
+
} @else {
|
|
242
|
+
<table>
|
|
243
|
+
...
|
|
244
|
+
</table>
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
With `placeholderData: keepPreviousData`, you'll usually want to show the
|
|
249
|
+
table even while refetching, plus an inline indicator:
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<table>
|
|
253
|
+
<thead>
|
|
254
|
+
...
|
|
255
|
+
</thead>
|
|
256
|
+
<tbody>
|
|
257
|
+
...
|
|
258
|
+
</tbody>
|
|
259
|
+
</table>
|
|
260
|
+
|
|
261
|
+
@if (todosQuery.isFetching()) {
|
|
262
|
+
<div class="refreshing-indicator">Refreshing…</div>
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`isPending()` is true only for the very first fetch; `isFetching()` is true
|
|
267
|
+
on every background refetch.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 5. Pagination button states
|
|
272
|
+
|
|
273
|
+
Under `manualPagination` + `keepPreviousData`, the next-page button should be
|
|
274
|
+
disabled when there's no more data — but `getCanNextPage()` only knows that
|
|
275
|
+
because you passed `rowCount`. Always pass it:
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<button
|
|
279
|
+
(click)="table.previousPage()"
|
|
280
|
+
[disabled]="!table.getCanPreviousPage() || todosQuery.isFetching()"
|
|
281
|
+
>
|
|
282
|
+
‹
|
|
283
|
+
</button>
|
|
284
|
+
<span
|
|
285
|
+
>Page {{ table.atoms.pagination.get().pageIndex + 1 }} of {{
|
|
286
|
+
table.getPageCount() }}</span
|
|
287
|
+
>
|
|
288
|
+
<button
|
|
289
|
+
(click)="table.nextPage()"
|
|
290
|
+
[disabled]="!table.getCanNextPage() || todosQuery.isFetching()"
|
|
291
|
+
>
|
|
292
|
+
›
|
|
293
|
+
</button>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Disabling buttons during `isFetching()` prevents double-clicks that fire a
|
|
297
|
+
second refetch.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 6. Row selection across refetches
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
// In the table options
|
|
305
|
+
getRowId: (row) => String(row.id),
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Plus register `rowSelectionFeature`. Now `RowSelectionState` is keyed by
|
|
309
|
+
`row.id` (the server primary key) — refetches that change row order don't
|
|
310
|
+
break selection. Without `getRowId`, IDs default to row index and selection
|
|
311
|
+
points at the wrong rows after a sort flip or refetch.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 7. Mutations (cell-level edits)
|
|
316
|
+
|
|
317
|
+
Use Query mutations for cell edits and invalidate the list query on success.
|
|
318
|
+
For inline editing UI, see `compose-with-tanstack-form` (when it ships) — or
|
|
319
|
+
the `examples/angular/editable/` pattern with `flexRenderComponent` and a
|
|
320
|
+
local edit signal.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { inject } from '@angular/core'
|
|
324
|
+
import { injectMutation, QueryClient } from '@tanstack/angular-query-experimental'
|
|
325
|
+
import { lastValueFrom } from 'rxjs'
|
|
326
|
+
|
|
327
|
+
private readonly queryClient = inject(QueryClient)
|
|
328
|
+
|
|
329
|
+
readonly toggleTodoMutation = injectMutation(() => ({
|
|
330
|
+
mutationFn: (id: number) =>
|
|
331
|
+
lastValueFrom(this.http.patch(`/todos/${id}`, { /* … */ })),
|
|
332
|
+
onSuccess: () => {
|
|
333
|
+
this.queryClient.invalidateQueries({ queryKey: ['todos'] })
|
|
334
|
+
},
|
|
335
|
+
}))
|
|
336
|
+
|
|
337
|
+
// In a cell:
|
|
338
|
+
cell: ({ row }) => flexRenderComponent(ToggleButton, {
|
|
339
|
+
inputs: { todo: row.original },
|
|
340
|
+
outputs: { toggle: (id) => this.toggleTodoMutation.mutate(id) },
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`invalidateQueries` triggers a background refetch; with `keepPreviousData`,
|
|
345
|
+
the user sees the existing list while the new one loads.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## 8. Should I use external `Store` atoms or Angular signals here?
|
|
350
|
+
|
|
351
|
+
For the filter / sort / pagination slices that the Query reads, **either
|
|
352
|
+
works**. The example above uses Angular signals because they read cleanly with
|
|
353
|
+
`@for` and `OnPush`, and Query's `queryKey` polls them on the next CD cycle.
|
|
354
|
+
|
|
355
|
+
Use an external TanStack Store atom (see `compose-with-tanstack-store`) when:
|
|
356
|
+
|
|
357
|
+
- The same slice must drive multiple tables.
|
|
358
|
+
- You're syncing the slice to the URL or `localStorage`.
|
|
359
|
+
- Other non-table consumers in the app already use the atom.
|
|
360
|
+
|
|
361
|
+
In those cases:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
import { paginationStore } from './stores'
|
|
365
|
+
import { injectSelector } from '@tanstack/angular-store'
|
|
366
|
+
|
|
367
|
+
readonly paginationSig = injectSelector(paginationStore, (s) => s)
|
|
368
|
+
|
|
369
|
+
readonly todosQuery = injectQuery(() => ({
|
|
370
|
+
queryKey: ['todos', this.paginationSig() /*, sort, filter */],
|
|
371
|
+
// ...
|
|
372
|
+
}))
|
|
373
|
+
|
|
374
|
+
readonly table = injectTable(() => ({
|
|
375
|
+
// ...
|
|
376
|
+
atoms: { pagination: paginationStore },
|
|
377
|
+
// no state.pagination needed — the atom owns it
|
|
378
|
+
}))
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Failure modes
|
|
384
|
+
|
|
385
|
+
### 1. (CRITICAL) `queryKey` missing the controlled signals
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
// ❌ Query won't refetch when pagination changes
|
|
389
|
+
queryKey: ['todos'],
|
|
390
|
+
|
|
391
|
+
// ✅
|
|
392
|
+
queryKey: ['todos', this.pagination(), this.sorting(), this.globalFilter()],
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Symptom: paginating "doesn't load the next page." Always include every signal
|
|
396
|
+
the request reads.
|
|
397
|
+
|
|
398
|
+
### 2. (CRITICAL) No `placeholderData: keepPreviousData` → "0 rows flash"
|
|
399
|
+
|
|
400
|
+
Without it, `data()` is `undefined` mid-refetch, the table renders 0 rows
|
|
401
|
+
for a frame. `keepPreviousData` (imported from
|
|
402
|
+
`@tanstack/angular-query-experimental`) keeps the last successful payload
|
|
403
|
+
visible until the new one resolves.
|
|
404
|
+
|
|
405
|
+
### 3. (CRITICAL) Forgetting `rowCount` under `manualPagination`
|
|
406
|
+
|
|
407
|
+
`getPageCount()` returns `-1`, "next" never disables. The server tells you
|
|
408
|
+
how many rows exist — pass it.
|
|
409
|
+
|
|
410
|
+
### 4. (CRITICAL) Keeping client row models for slices the server owns
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
// ❌ Double-processes the data
|
|
414
|
+
manualPagination: true,
|
|
415
|
+
rowModels: { paginatedRowModel: createPaginatedRowModel() }, // re-paginates server page
|
|
416
|
+
|
|
417
|
+
// ✅
|
|
418
|
+
rowModels: {} // (or just keep the ones still client-side)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Same applies to `sortedRowModel` under `manualSorting` and `filteredRowModel`
|
|
422
|
+
under `manualFiltering`.
|
|
423
|
+
|
|
424
|
+
### 5. (CRITICAL) Missing `getRowId` with row selection
|
|
425
|
+
|
|
426
|
+
Selection is keyed by row ID. Index-as-ID breaks when the server returns rows
|
|
427
|
+
in a new order. `getRowId: (row) => row.id`.
|
|
428
|
+
|
|
429
|
+
### 6. (HIGH) `autoResetPageIndex` bouncing the user to page 0 on every refetch
|
|
430
|
+
|
|
431
|
+
Every Query response is a new array reference → table sees "new data" → resets
|
|
432
|
+
`pageIndex`. Set `autoResetPageIndex: false` and reset explicitly when you
|
|
433
|
+
want to (e.g. in `onGlobalFilterChange`).
|
|
434
|
+
|
|
435
|
+
### 7. (HIGH) Refetching on every keystroke
|
|
436
|
+
|
|
437
|
+
Typing into the global filter fires a fetch per character. Debounce: either
|
|
438
|
+
keep a separate `globalFilterInput` signal and propagate to `globalFilter` on
|
|
439
|
+
a delay, or compose with `@tanstack/angular-pacer` (when its skill ships).
|
|
440
|
+
Also reset `pageIndex: 0` on filter change.
|
|
441
|
+
|
|
442
|
+
### 8. (HIGH) Forgetting the updater-fn branch in `on[State]Change`
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
// ❌ Crashes when TanStack Table passes a function (e.g. table.setPageIndex((p) => p + 1))
|
|
446
|
+
onPaginationChange: (value) => this.pagination.set(value)
|
|
447
|
+
|
|
448
|
+
// ✅
|
|
449
|
+
onPaginationChange: (u) =>
|
|
450
|
+
typeof u === 'function' ? this.pagination.update(u) : this.pagination.set(u)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### 9. (MEDIUM) Reaching for `effect(...)` to call `query.refetch()` on signal changes
|
|
454
|
+
|
|
455
|
+
Don't. The whole point of `queryKey` is that Query refetches when the key
|
|
456
|
+
changes. Adding an `effect` that calls `refetch()` produces double fetches and
|
|
457
|
+
race conditions. Trust the key.
|
|
458
|
+
|
|
459
|
+
### 10. (MEDIUM) Two sources of truth for filter state
|
|
460
|
+
|
|
461
|
+
Common bug: keep a `signal('')` for the input AND a `state.globalFilter`
|
|
462
|
+
controlled value, and try to sync them via `effect`. Pick one: the table's
|
|
463
|
+
`globalFilter` is fine for both UI and server query. If you need debouncing,
|
|
464
|
+
use an _additional_ `globalFilterInput` signal for the raw input and update
|
|
465
|
+
the table-controlled signal on a delay.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## See also
|
|
470
|
+
|
|
471
|
+
- `tanstack-table/angular/client-to-server` — the no-Query baseline using
|
|
472
|
+
`rxResource` / `httpResource`
|
|
473
|
+
- `tanstack-table/angular/table-state` — Angular signal + `state` + `on*Change`
|
|
474
|
+
- `tanstack-table/angular/compose-with-tanstack-store` — when to use shared
|
|
475
|
+
atoms instead of signals
|
|
476
|
+
- `tanstack-table/core/filtering` — manualFiltering semantics
|
|
477
|
+
- `tanstack-table/core/sorting` — manualSorting semantics
|
|
478
|
+
- `tanstack-table/core/pagination` — manualPagination + `rowCount`
|
|
479
|
+
- Example: `examples/angular/remote-data/` — analogous pattern with
|
|
480
|
+
`rxResource` instead of `injectQuery`
|
|
481
|
+
- `@tanstack/angular-query-experimental` docs for `injectQuery`,
|
|
482
|
+
`injectMutation`, `injectInfiniteQuery`, `QueryClient`
|