pbtsdb 0.11.0 → 2.0.0
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 +76 -57
- package/dist/chunk-4CRCARB3.js +2954 -0
- package/dist/chunk-4CRCARB3.js.map +1 -0
- package/dist/core.d.ts +180 -86
- package/dist/core.js +1 -1
- package/dist/index.d.ts +6 -10
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/llms.txt +29 -25
- package/package.json +6 -11
- package/dist/chunk-OBDOVDCI.js +0 -1596
- package/dist/chunk-OBDOVDCI.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# pbtsdb: PocketBase TanStack Database Integration
|
|
2
2
|
|
|
3
|
-
> Type-safe PocketBase integration with TanStack
|
|
3
|
+
> Type-safe PocketBase integration with TanStack DB
|
|
4
4
|
|
|
5
|
-
A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.io) with [TanStack
|
|
5
|
+
A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.io) with [TanStack DB](https://tanstack.com/db), providing:
|
|
6
6
|
|
|
7
7
|
- 🔥 **Real-time subscriptions** with automatic synchronization
|
|
8
8
|
- 🎯 **Full TypeScript type safety** for queries and relations
|
|
9
9
|
- ⚡ **Reactive collections** with TanStack DB
|
|
10
|
-
- 🔄 **Automatic caching** via TanStack Query
|
|
11
10
|
- ✨ **Optimistic mutations** with insert/update/delete support
|
|
12
11
|
- 🎨 **React hooks** for easy component integration
|
|
13
12
|
- 🔗 **Type-safe joins** and relation fetching into their own collections
|
|
@@ -32,16 +31,14 @@ A TypeScript library that seamlessly integrates [PocketBase](https://pocketbase.
|
|
|
32
31
|
## Installation
|
|
33
32
|
|
|
34
33
|
```bash
|
|
35
|
-
npm install pbtsdb pocketbase @tanstack/db @tanstack/
|
|
34
|
+
npm install pbtsdb pocketbase @tanstack/db @tanstack/react-db
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
### Peer Dependencies
|
|
39
38
|
|
|
40
39
|
- `pocketbase` >= 0.22.0
|
|
41
|
-
- `@tanstack/db` >= 0.
|
|
42
|
-
- `@tanstack/
|
|
43
|
-
- `@tanstack/react-query` >= 5.0.0
|
|
44
|
-
- `@tanstack/react-db` >= 0.1.86 (optional; only for `createReactProvider`)
|
|
40
|
+
- `@tanstack/db` >= 0.12.1
|
|
41
|
+
- `@tanstack/react-db` >= 0.5.5 (optional; only for `createReactProvider`)
|
|
45
42
|
- `react` and `react-dom` >= 18.0.0 (optional)
|
|
46
43
|
|
|
47
44
|
All peer dependencies use minimum version constraints; newer versions should work. The
|
|
@@ -110,18 +107,12 @@ type BlogSchema = {
|
|
|
110
107
|
```typescript
|
|
111
108
|
// app.tsx
|
|
112
109
|
import PocketBase from 'pocketbase';
|
|
113
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
114
110
|
import { createCollection, createReactProvider } from 'pbtsdb';
|
|
115
111
|
|
|
116
112
|
const pb = new PocketBase('http://localhost:8090');
|
|
117
|
-
const queryClient = new QueryClient({
|
|
118
|
-
defaultOptions: {
|
|
119
|
-
queries: { staleTime: 60_000 } // Cache for 1 minute
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
113
|
|
|
123
114
|
// Create collections with automatic type inference
|
|
124
|
-
const c = createCollection<BlogSchema>(pb
|
|
115
|
+
const c = createCollection<BlogSchema>(pb);
|
|
125
116
|
const users = c('users', {});
|
|
126
117
|
export const { Provider, useStore } = createReactProvider({
|
|
127
118
|
users,
|
|
@@ -139,11 +130,9 @@ export const { Provider, useStore } = createReactProvider({
|
|
|
139
130
|
|
|
140
131
|
export function App() {
|
|
141
132
|
return (
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
</Provider>
|
|
146
|
-
</QueryClientProvider>
|
|
133
|
+
<Provider>
|
|
134
|
+
<BlogDashboard />
|
|
135
|
+
</Provider>
|
|
147
136
|
);
|
|
148
137
|
}
|
|
149
138
|
```
|
|
@@ -253,14 +242,13 @@ Collections are reactive data stores that automatically sync with PocketBase:
|
|
|
253
242
|
|
|
254
243
|
```typescript
|
|
255
244
|
// Create a collection using the curried API
|
|
256
|
-
const c = createCollection<MySchema>(pb
|
|
245
|
+
const c = createCollection<MySchema>(pb);
|
|
257
246
|
const booksCollection = c('books', {});
|
|
258
247
|
|
|
259
248
|
// Collections automatically:
|
|
260
249
|
// - Fetch data from PocketBase
|
|
261
250
|
// - Subscribe to real-time updates
|
|
262
251
|
// - Update React components when data changes
|
|
263
|
-
// - Cache data via TanStack Query
|
|
264
252
|
```
|
|
265
253
|
|
|
266
254
|
### Real-time Subscriptions
|
|
@@ -269,7 +257,7 @@ Collections manage subscriptions **automatically** based on query lifecycle:
|
|
|
269
257
|
|
|
270
258
|
```typescript
|
|
271
259
|
// Collections are lazy - no subscription until queried
|
|
272
|
-
const c = createCollection<MySchema>(pb
|
|
260
|
+
const c = createCollection<MySchema>(pb);
|
|
273
261
|
const booksCollection = c('books', {});
|
|
274
262
|
|
|
275
263
|
// Subscription starts automatically when query becomes active
|
|
@@ -286,6 +274,31 @@ const { data } = useLiveQuery((q) =>
|
|
|
286
274
|
- **Shared:** Multiple components using the same collection share one subscription
|
|
287
275
|
- **No manual control needed:** The collection handles all subscription management internally
|
|
288
276
|
|
|
277
|
+
### Reconnects
|
|
278
|
+
|
|
279
|
+
pbtsdb runs its own realtime connection. When the connection drops, it reconnects with backoff and refetches every live query, because PocketBase does not replay events missed during the gap. A server that supports pbtsdb's resume extension (`?resume=<clientId>&after=<seq>` on the SSE URL, `resumed: true` in `PB_CONNECT`, `seq` on each event) replays the gap instead, and no refetch runs.
|
|
280
|
+
|
|
281
|
+
pbtsdb's connection is independent of `pb.realtime`. When `pb.authStore` changes to another auth record (login, logout, switching users), pbtsdb forgets the connection's server-side session and reconnects at once, re-sending every subscribed topic under the new auth; every ready collection reloads once that POST succeeds, the same as a non-resumed reconnect. A token refresh for the same record changes nothing. PocketBase would otherwise answer the next subscriptions POST with a 403, or keep serving the previous user's data.
|
|
282
|
+
|
|
283
|
+
Call `resetRealtime(pb)` yourself for a change the auth store cannot see, such as pointing `pb` at another server:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { disconnectRealtime, resetRealtime } from 'pbtsdb';
|
|
287
|
+
|
|
288
|
+
resetRealtime(pb); // forget the session and re-subscribe every open topic now
|
|
289
|
+
|
|
290
|
+
disconnectRealtime(pb); // close the connection and keep it closed
|
|
291
|
+
resetRealtime(pb); // open it again; every ready collection reloads
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`disconnectRealtime(pb)` closes the connection and keeps it closed until `resetRealtime(pb)`. Collections keep working over REST and keep their subscriptions registered. Use it at logout, or at startup where realtime is not wanted, such as an embedded view.
|
|
295
|
+
|
|
296
|
+
### Subset Lifetime
|
|
297
|
+
|
|
298
|
+
In on-demand mode a live query's subset is released when the last subscriber unmounts, after a grace window of `subsetGcTime` milliseconds (default 5000). A query with an equal request that mounts within the window reuses the rows with no request. Realtime keeps the parked rows fresh, and rows a parent filed through a relation stay for the same window, so a panel that mounts and unmounts quickly costs no refetch. `collection.reload()` releases every parked subset. An auth change releases every waiting subset and every accepted row at once and reloads every live subset under the new auth, whether or not realtime is connected, so a load right after a logout never reuses the previous user's rows and the order of `authStore.clear()` and `disconnectRealtime(pb)` does not matter. Set `subsetGcTime: 0` to release a subset as soon as it unloads; `0` also keeps accepted rows until a reload or idle instead of expiring them.
|
|
299
|
+
|
|
300
|
+
A subset load that fails is retried with backoff (`loadRetryDelays`, default 1, 2, 4, 8, 15 and then every 30 seconds) until it succeeds, so a live query stays loading through an outage instead of entering an error state it cannot leave. A `collection.reload()` or a realtime reconnect retries at once, also while another reload is running. A response the server gave on purpose, a 4xx other than 401, 408 or 429, is not retried and reports its error. A 401 waits for the next auth change and then retries. Delays carry a little jitter so many queries do not retry in lockstep.
|
|
301
|
+
|
|
289
302
|
### Sync Modes
|
|
290
303
|
|
|
291
304
|
Every collection is either **eager** (the default) or **on-demand**:
|
|
@@ -331,9 +344,9 @@ builder still loads the first `n + limit` rows. Put a page boundary in the
|
|
|
331
344
|
|
|
332
345
|
### Mutations and Refetch
|
|
333
346
|
|
|
334
|
-
|
|
347
|
+
The built-in handlers write PocketBase's response into the collection before they return: server-assigned fields like `created` and `updated`, and any values rewritten by PocketBase hooks. A built-in delete removes the row the same way. TanStack DB drops a mutation's optimistic state when its handler settles, and rows written while the handler runs publish together with that drop, so the settled row is the server's row with no gap. The realtime echo that follows changes nothing.
|
|
335
348
|
|
|
336
|
-
Set `refetchOnMutation: true` to
|
|
349
|
+
By default, pbtsdb does **not** refetch after a successful insert, update, or delete. Set `refetchOnMutation: true` to refetch the collection's active queries before the handler settles — for example, when a server-side hook changes other rows you must read right after the mutation:
|
|
337
350
|
|
|
338
351
|
```typescript
|
|
339
352
|
const collection = c('books', {
|
|
@@ -341,7 +354,23 @@ const collection = c('books', {
|
|
|
341
354
|
});
|
|
342
355
|
```
|
|
343
356
|
|
|
344
|
-
The option only affects the built-in default handlers.
|
|
357
|
+
The option only affects the built-in default handlers. A custom `onInsert`, `onUpdate`, or `onDelete` controls write-back itself:
|
|
358
|
+
|
|
359
|
+
- TanStack DB drops the optimistic state when your handler returns. Until the realtime echo arrives, the row shows its previous server value: an updated row reverts, an inserted row disappears, and a deleted row comes back.
|
|
360
|
+
- For an insert or an update, `await collection.accept(serverRows)` before you return. This closes the gap.
|
|
361
|
+
- For a delete, `await collection.evict(ids)` before you return. Without it, the deleted row shows again until the delete echo arrives.
|
|
362
|
+
- In on-demand mode, when realtime goes idle (no live query on the collection), rows held only by an accepted write-back are released. A row written back while nothing is mounted is fetched again on the next mount.
|
|
363
|
+
- The handler returns `void`. Core no longer reads a `{ refetch }` result; call `collection.reload()` yourself if the handler needs a refetch.
|
|
364
|
+
|
|
365
|
+
### Writing server rows yourself
|
|
366
|
+
|
|
367
|
+
`collection.accept(rows)` lands rows the server returned, for example the response of a custom endpoint, as confirmed state. A row older than the stored one is ignored. Use it when the screen must update before the realtime echo arrives. It resolves when the rows are accepted, so a custom mutation handler can await it.
|
|
368
|
+
|
|
369
|
+
`collection.evict(ids)` removes rows the server deleted, for example after a custom endpoint deleted them. The rows leave every holder, and a fetch in flight does not put them back. It resolves when the removal is accepted, so a custom delete handler can await it.
|
|
370
|
+
|
|
371
|
+
`collection.reload()` refetches every live query's subset. Then it releases the realtime-topic and accepted holders of the rows the results do not confirm. A row that a parent filed through a relation stays. Use it when the server state changed with no realtime event, such as after a user loses access to rows.
|
|
372
|
+
|
|
373
|
+
All three are also on `collection.utils`.
|
|
345
374
|
|
|
346
375
|
### Type Safety
|
|
347
376
|
|
|
@@ -367,7 +396,6 @@ The main function for creating type-safe collections. Uses a curried API for bet
|
|
|
367
396
|
```typescript
|
|
368
397
|
const c = createCollection<Schema>(
|
|
369
398
|
pb: PocketBase,
|
|
370
|
-
queryClient: QueryClient,
|
|
371
399
|
factoryOptions?: CreateCollectionFactoryOptions
|
|
372
400
|
);
|
|
373
401
|
const collection = c(collectionName: string, options?: CreateCollectionOptions);
|
|
@@ -375,7 +403,6 @@ const collection = c(collectionName: string, options?: CreateCollectionOptions);
|
|
|
375
403
|
|
|
376
404
|
**Parameters:**
|
|
377
405
|
- `pb` - PocketBase instance
|
|
378
|
-
- `queryClient` - TanStack Query QueryClient instance
|
|
379
406
|
- `factoryOptions` - Optional configuration applied to every collection this factory builds (see [Subscription Options](#subscription-options))
|
|
380
407
|
- `collectionName` - Name of the PocketBase collection
|
|
381
408
|
- `options` - Optional configuration
|
|
@@ -390,7 +417,6 @@ const collection = c(collectionName: string, options?: CreateCollectionOptions);
|
|
|
390
417
|
- `onUpdate?: UpdateMutationFn | false` - Custom update handler or `false` to disable
|
|
391
418
|
- `onDelete?: DeleteMutationFn | false` - Custom delete handler or `false` to disable
|
|
392
419
|
- `refetchOnMutation?: boolean` - Refetch the collection after a built-in insert/update/delete succeeds (default: `false`; see [Mutations and Refetch](#mutations-and-refetch))
|
|
393
|
-
- `ignoreAutoCancellation?: boolean` - Ignore PocketBase auto-cancellation errors (default: `true`)
|
|
394
420
|
- `collectionOptions?: object` - Additional TanStack DB collection options passed through directly (see [Collection Options Passthrough](#collection-options-passthrough))
|
|
395
421
|
|
|
396
422
|
**Returns:** Fully-typed Collection instance with subscription capabilities
|
|
@@ -399,7 +425,7 @@ const collection = c(collectionName: string, options?: CreateCollectionOptions);
|
|
|
399
425
|
|
|
400
426
|
Basic collection (lazy, subscribes automatically on first query):
|
|
401
427
|
```typescript
|
|
402
|
-
const c = createCollection<MySchema>(pb
|
|
428
|
+
const c = createCollection<MySchema>(pb);
|
|
403
429
|
const booksCollection = c('books', {});
|
|
404
430
|
```
|
|
405
431
|
|
|
@@ -410,7 +436,7 @@ collections. Rows never carry `expand`; read related records from the target
|
|
|
410
436
|
collection.
|
|
411
437
|
|
|
412
438
|
```typescript
|
|
413
|
-
const c = createCollection<MySchema>(pb
|
|
439
|
+
const c = createCollection<MySchema>(pb);
|
|
414
440
|
const authors = c('authors', { syncMode: 'on-demand' });
|
|
415
441
|
const tags = c('tags', { syncMode: 'on-demand' });
|
|
416
442
|
const books = c('books', {
|
|
@@ -421,8 +447,8 @@ const books = c('books', {
|
|
|
421
447
|
|
|
422
448
|
Every books request expands `author`; the expanded authors are filed into
|
|
423
449
|
`authors` (an on-demand target has its sync started) and removed from the
|
|
424
|
-
book rows. Read them through `materialize()` in a query
|
|
425
|
-
`
|
|
450
|
+
book rows. Read them through `materialize()` in a query or through a join,
|
|
451
|
+
inside the `useLiveQuery` that needs them:
|
|
426
452
|
|
|
427
453
|
```typescript
|
|
428
454
|
import { eq } from '@tanstack/db';
|
|
@@ -446,6 +472,14 @@ reads through a `fetchRelations()` view, whose fetch goes to PocketBase so its
|
|
|
446
472
|
paths get filed; anything else is fetched in one batched request. An empty
|
|
447
473
|
`inArray(id, [])` yields no rows and no request.
|
|
448
474
|
|
|
475
|
+
Do not read a related row with `authors.get(book.author)` in a component. A
|
|
476
|
+
`get()` is a one-time read: the component does not re-render when the author
|
|
477
|
+
changes, and the row can leave the store while the component still shows it,
|
|
478
|
+
because a filed row stays only as long as a live query holds the parent row
|
|
479
|
+
that filed it. A `useLiveQuery` with `materialize()` or a join holds the rows
|
|
480
|
+
it reads and re-renders when they change. `get()` is for code outside React
|
|
481
|
+
that needs a value right now, such as a mutation handler or a test.
|
|
482
|
+
|
|
449
483
|
Fetch a relation for one query only with `fetchRelations()`; the view shares the
|
|
450
484
|
collection's store, realtime subscription, and mutations, and only its fetches
|
|
451
485
|
add the `expand` parameter:
|
|
@@ -490,7 +524,7 @@ never treated as complete.
|
|
|
490
524
|
Pass any [TanStack DB `BaseCollectionConfig`](https://tanstack.com/db/latest/docs/overview) option directly via `collectionOptions`. This is useful for configuring indexing, garbage collection, and other collection-level settings:
|
|
491
525
|
|
|
492
526
|
```typescript
|
|
493
|
-
const c = createCollection<MySchema>(pb
|
|
527
|
+
const c = createCollection<MySchema>(pb);
|
|
494
528
|
const booksCollection = c('books', {
|
|
495
529
|
collectionOptions: {
|
|
496
530
|
gcTime: 60000, // 1 minute GC
|
|
@@ -504,7 +538,7 @@ pbtsdb defaults `autoIndex` to `'eager'` with `defaultIndexType: BTreeIndex`, so
|
|
|
504
538
|
TanStack DB does not warn about a missing index). Pass `autoIndex: 'off'` or a
|
|
505
539
|
different `defaultIndexType` in `collectionOptions` to change that per collection.
|
|
506
540
|
|
|
507
|
-
The following fields are managed by pbtsdb and excluded from `collectionOptions`: `getKey`, `syncMode`, `onInsert`, `onUpdate`, `onDelete`, `schema`.
|
|
541
|
+
The following fields are managed by pbtsdb and excluded from `collectionOptions`: `getKey`, `syncMode`, `onInsert`, `onUpdate`, `onDelete`, `schema`, `utils`.
|
|
508
542
|
|
|
509
543
|
### React Integration
|
|
510
544
|
|
|
@@ -527,7 +561,7 @@ const { Provider, useStore } = createReactProvider(collections: CollectionsMap);
|
|
|
527
561
|
```typescript
|
|
528
562
|
import { createCollection, createReactProvider } from 'pbtsdb';
|
|
529
563
|
|
|
530
|
-
const c = createCollection<MySchema>(pb
|
|
564
|
+
const c = createCollection<MySchema>(pb);
|
|
531
565
|
const collections = {
|
|
532
566
|
authors: c('authors', {}),
|
|
533
567
|
books: c('books', {
|
|
@@ -685,7 +719,7 @@ options — `headers`, `filter`, `expand`, `fields` — to every real-time
|
|
|
685
719
|
subscription the factory creates.
|
|
686
720
|
|
|
687
721
|
```typescript
|
|
688
|
-
const c = createCollection<Schema>(pb,
|
|
722
|
+
const c = createCollection<Schema>(pb, {
|
|
689
723
|
subscribeOptions: () => {
|
|
690
724
|
const token = getShareToken();
|
|
691
725
|
return token ? { headers: { 'X-Share-Token': token } } : undefined;
|
|
@@ -968,7 +1002,7 @@ export function CreateBookForm() {
|
|
|
968
1002
|
try {
|
|
969
1003
|
// Optimistic insert - appears instantly
|
|
970
1004
|
const tx = books.insert({ id: newRecordId(), title, author: 'author_id' });
|
|
971
|
-
await tx.
|
|
1005
|
+
await tx.when('settled');
|
|
972
1006
|
|
|
973
1007
|
if (tx.state === 'completed') setTitle('');
|
|
974
1008
|
else setError('Failed to create book');
|
|
@@ -1143,7 +1177,7 @@ Always create collections with proper type parameters:
|
|
|
1143
1177
|
|
|
1144
1178
|
```typescript
|
|
1145
1179
|
// ✅ Good - full type safety
|
|
1146
|
-
const c = createCollection<MySchema>(pb
|
|
1180
|
+
const c = createCollection<MySchema>(pb);
|
|
1147
1181
|
const books = c('books', {
|
|
1148
1182
|
omitOnInsert: ['created', 'updated'] as const
|
|
1149
1183
|
});
|
|
@@ -1164,7 +1198,7 @@ Define all collections once at app initialization:
|
|
|
1164
1198
|
|
|
1165
1199
|
```typescript
|
|
1166
1200
|
// ✅ Do this - centralized, type-safe
|
|
1167
|
-
const c = createCollection<MySchema>(pb
|
|
1201
|
+
const c = createCollection<MySchema>(pb);
|
|
1168
1202
|
|
|
1169
1203
|
export const { Provider, useStore } = createReactProvider({
|
|
1170
1204
|
posts: c('posts', { omitOnInsert: ['created', 'updated'] as const }),
|
|
@@ -1179,7 +1213,7 @@ When declaring `relations`, create the target collection first:
|
|
|
1179
1213
|
|
|
1180
1214
|
```typescript
|
|
1181
1215
|
// ✅ Good - authors exists before books references it
|
|
1182
|
-
const c = createCollection<MySchema>(pb
|
|
1216
|
+
const c = createCollection<MySchema>(pb);
|
|
1183
1217
|
const authors = c('authors', {});
|
|
1184
1218
|
const books = c('books', {
|
|
1185
1219
|
relations: { author: authors }, // authors is already created
|
|
@@ -1230,7 +1264,7 @@ return <PostsList posts={data} />;
|
|
|
1230
1264
|
per parent row, on every fetch of the parent:
|
|
1231
1265
|
|
|
1232
1266
|
```typescript
|
|
1233
|
-
const c = createCollection<MySchema>(pb
|
|
1267
|
+
const c = createCollection<MySchema>(pb);
|
|
1234
1268
|
const authors = c('authors', {});
|
|
1235
1269
|
const posts = c('posts', {
|
|
1236
1270
|
relations: { author: authors },
|
|
@@ -1246,20 +1280,6 @@ it) and, once the rows are filed, subsequent queries make no request at all.
|
|
|
1246
1280
|
Prefer `alwaysFetchRelations` when the parent is the only path by which those
|
|
1247
1281
|
rows enter an on-demand collection; otherwise let the query load them.
|
|
1248
1282
|
|
|
1249
|
-
### 6. Configure QueryClient Defaults
|
|
1250
|
-
|
|
1251
|
-
```typescript
|
|
1252
|
-
const queryClient = new QueryClient({
|
|
1253
|
-
defaultOptions: {
|
|
1254
|
-
queries: {
|
|
1255
|
-
staleTime: 60_000, // 1 minute
|
|
1256
|
-
gcTime: 300_000, // 5 minutes
|
|
1257
|
-
refetchOnWindowFocus: false
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
```
|
|
1262
|
-
|
|
1263
1283
|
## Configuration
|
|
1264
1284
|
|
|
1265
1285
|
### Custom Logger Integration
|
|
@@ -1381,6 +1401,5 @@ npm run typecheck # TypeScript only
|
|
|
1381
1401
|
|
|
1382
1402
|
**Built with:**
|
|
1383
1403
|
- [PocketBase](https://pocketbase.io) - Backend-as-a-Service
|
|
1384
|
-
- [TanStack Query](https://tanstack.com/query) - Powerful data fetching
|
|
1385
1404
|
- [TanStack DB](https://tanstack.com/db) - Reactive database
|
|
1386
1405
|
- [TypeScript](https://www.typescriptlang.org) - Type safety
|