@taladb/react 0.10.2 → 0.11.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/LICENSE-MIT +21 -0
- package/README.md +187 -0
- package/dist/chunk-SEH233OC.mjs +129 -0
- package/dist/index.d.mts +40 -309
- package/dist/index.d.ts +40 -309
- package/dist/index.js +23 -575
- package/dist/index.mjs +32 -707
- package/dist/query/index.d.mts +1664 -0
- package/dist/query/index.d.ts +1664 -0
- package/dist/query/index.js +2226 -0
- package/dist/query/index.mjs +2091 -0
- package/package.json +14 -5
- /package/{LICENSE → LICENSE-APACHE} +0 -0
package/LICENSE-MIT
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 taladb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @taladb/react
|
|
2
|
+
|
|
3
|
+
React and React Native hooks for TalaDB — live queries that re-render your components when the local database changes.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@taladb/react)
|
|
6
|
+
[](https://github.com/taladb/taladb/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
## What this gives you
|
|
9
|
+
|
|
10
|
+
TalaDB collections are already reactive — `col.subscribe(filter, cb)` pushes a fresh snapshot on every matching change. These hooks wire that into React's rendering model for you: subscription lifecycle, snapshot identity, and cleanup, so a write in one component re-renders every other component reading the same data.
|
|
11
|
+
|
|
12
|
+
Reads are backed by `useSyncExternalStore`, so snapshots never tear under concurrent rendering.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { TalaDBProvider, useCollection, useFind, useWrite } from '@taladb/react'
|
|
16
|
+
|
|
17
|
+
function App() {
|
|
18
|
+
return (
|
|
19
|
+
<TalaDBProvider name="myapp.db" fallback={<Spinner />}>
|
|
20
|
+
<Articles />
|
|
21
|
+
</TalaDBProvider>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Articles() {
|
|
26
|
+
const articles = useCollection<Article>('articles')
|
|
27
|
+
const { data, loading } = useFind(articles, { published: true })
|
|
28
|
+
const { write, pending } = useWrite<Article>({ collection: 'articles' })
|
|
29
|
+
|
|
30
|
+
if (loading) return <Spinner />
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{data.map((a) => (
|
|
35
|
+
<h2 key={a._id}>{a.title}</h2>
|
|
36
|
+
))}
|
|
37
|
+
<button
|
|
38
|
+
disabled={pending}
|
|
39
|
+
onClick={() => write({ type: 'insert', doc: { title: 'New', published: false } })}
|
|
40
|
+
>
|
|
41
|
+
Add
|
|
42
|
+
</button>
|
|
43
|
+
</>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
There is no cache to invalidate and no refetch to trigger. The write commits locally and the list above re-renders.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm add @taladb/react taladb
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Plus the platform backend TalaDB needs — `@taladb/web` in the browser, `@taladb/react-native` on device, `@taladb/node` on the server. See the [taladb](https://www.npmjs.com/package/taladb) package.
|
|
57
|
+
|
|
58
|
+
**Peer dependencies:** `react >= 18`, `taladb ^0.11.0`.
|
|
59
|
+
|
|
60
|
+
## Provider
|
|
61
|
+
|
|
62
|
+
`TalaDBProvider` takes one of two shapes.
|
|
63
|
+
|
|
64
|
+
**Provider-owned** — pass a `name` and it owns the `openDB` lifecycle: opens lazily on the client, never during SSR, and closes on unmount. This is the right form for Next.js.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<TalaDBProvider name="myapp.db" options={{ /* OpenDBOptions */ }} fallback={<Spinner />}>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Children render only once the database is ready, so `useTalaDB()` never observes a missing instance. `fallback` renders until then (and during SSR); it defaults to `null`.
|
|
71
|
+
|
|
72
|
+
**Caller-owned** — pass a `db` you opened yourself, and you keep the lifecycle.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
const db = await openDB('myapp.db')
|
|
76
|
+
<TalaDBProvider db={db}>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Registering collection options
|
|
80
|
+
|
|
81
|
+
Per-collection options (`schema`, `syncSchema`, `migrateDocument`, …) belong on the provider, keyed by collection name:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<TalaDBProvider
|
|
85
|
+
name="myapp.db"
|
|
86
|
+
collections={{
|
|
87
|
+
articles: { schema: ArticleSchema, syncSchema: { version: 1 } },
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This matters: without the registry, `useCollection` resolves a bare `db.collection(name)` handle, and writes through `useWrite` silently skip `schema` validation and the `_v` shape stamp. The registry is read when a handle is first created and treated as static, so an inline object here won't thrash live queries.
|
|
93
|
+
|
|
94
|
+
## Hooks
|
|
95
|
+
|
|
96
|
+
### `useCollection<T>(name, options?)`
|
|
97
|
+
|
|
98
|
+
Resolves a collection from the provider's database, applying the registered options for `name`. The handle is memoized — same reference every render unless the db or name changes — so pass it straight to `useFind` without wrapping in `useMemo`. A per-call `options` argument overrides the registry entry.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const articles = useCollection<Article>('articles')
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `useFind<T>(collection, filter?)`
|
|
105
|
+
|
|
106
|
+
Subscribes to a live query.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const { data, loading, error } = useFind(articles, { locale: 'en' })
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
| Field | Type | |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `data` | `T[]` | Matching documents. Empty while loading. |
|
|
115
|
+
| `loading` | `boolean` | True until the first snapshot arrives. |
|
|
116
|
+
| `error` | `unknown \| null` | Last subscription error, cleared by the next good snapshot. |
|
|
117
|
+
|
|
118
|
+
Inline filter objects are safe — the filter is serialized for subscription identity, so `{ active: true }` on every render does not re-subscribe.
|
|
119
|
+
|
|
120
|
+
### `useFindOne<T>(collection, filter)`
|
|
121
|
+
|
|
122
|
+
Same, for a single document. `data` is `T | null` — `null` both while loading and when nothing matched.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const { data: article, loading } = useFindOne(articles, { slug })
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `useAggregate<T, R>(collection, pipeline)`
|
|
129
|
+
|
|
130
|
+
A live aggregation, and **the paging primitive** — `find()` has no sort, skip, or limit, so reach for this when you need them.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const { data, loading } = useAggregate<Article>(articles, [
|
|
134
|
+
{ $match: { published: true } },
|
|
135
|
+
{ $sort: { createdAt: -1 } },
|
|
136
|
+
{ $skip: page * 20 },
|
|
137
|
+
{ $limit: 20 },
|
|
138
|
+
])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Inline pipeline arrays are serialized for identity, the same as filters.
|
|
142
|
+
|
|
143
|
+
### `useWrite<T>({ collection })`
|
|
144
|
+
|
|
145
|
+
Writes to a collection. The write is local, immediate, and durable — every `useFind` / `useFindOne` / `useAggregate` on that collection re-renders once it commits.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const { write, writeAsync, pending, error } = useWrite<Article>({ collection: 'articles' })
|
|
149
|
+
|
|
150
|
+
write({ type: 'insert', doc: { title: 'Hello', published: false } })
|
|
151
|
+
write({ type: 'update', where: { _id: id }, set: { published: true } })
|
|
152
|
+
write({ type: 'delete', where: { _id: id } })
|
|
153
|
+
|
|
154
|
+
await writeAsync({ type: 'insert', doc }) // rejects on error
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`write` is fire-and-forget — failures land on `error` rather than being thrown into render. `writeAsync` resolves once the write commits and rejects on failure. `pending` is true while a write is in flight.
|
|
158
|
+
|
|
159
|
+
It's named `useWrite`, not `useMutation`, because that's all it is: a local write with no network step and no rollback to reason about. The database is on the device — the write either committed or threw.
|
|
160
|
+
|
|
161
|
+
### `useTalaDB()`
|
|
162
|
+
|
|
163
|
+
The `TalaDB` instance from the nearest provider, for anything the hooks above don't cover.
|
|
164
|
+
|
|
165
|
+
## Server rendering
|
|
166
|
+
|
|
167
|
+
`openDB` cannot run during server rendering. Use the provider-owned form (`name=`), which opens only on the client and renders `fallback` until the handle exists. Hooks below it never see a null database.
|
|
168
|
+
|
|
169
|
+
## `@taladb/react/query`
|
|
170
|
+
|
|
171
|
+
A subpath export for a local-first data layer over TalaDB — cache-as-a-real-collection, background revalidation, writes that drain to the network without blocking the UI.
|
|
172
|
+
|
|
173
|
+
> **Status: in progress.** The type surface, `defineBackend`, and the envelope and query-record layer are in place. The hooks are not implemented yet — don't build on it.
|
|
174
|
+
|
|
175
|
+
## Documentation
|
|
176
|
+
|
|
177
|
+
**[https://taladb.dev](https://taladb.dev)**
|
|
178
|
+
|
|
179
|
+
- [React Guide](https://taladb.dev/guide/react)
|
|
180
|
+
- [React Native Guide](https://taladb.dev/guide/react-native)
|
|
181
|
+
- [Live Queries](https://taladb.dev/api/live-queries)
|
|
182
|
+
- [Schema Validation](https://taladb.dev/api/schema)
|
|
183
|
+
- [Filters](https://taladb.dev/api/filters) · [Aggregation](https://taladb.dev/api/aggregation)
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
Apache 2.0 © [TalaDB](https://github.com/taladb)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/context.tsx
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState
|
|
11
|
+
} from "react";
|
|
12
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
13
|
+
var TalaDBContext = createContext(null);
|
|
14
|
+
var CollectionOptionsContext = createContext({
|
|
15
|
+
get: () => void 0,
|
|
16
|
+
names: () => []
|
|
17
|
+
});
|
|
18
|
+
function useCollectionOptions() {
|
|
19
|
+
return useContext(CollectionOptionsContext);
|
|
20
|
+
}
|
|
21
|
+
function CollectionOptionsProvider({
|
|
22
|
+
collections,
|
|
23
|
+
children
|
|
24
|
+
}) {
|
|
25
|
+
const latest = useRef(collections);
|
|
26
|
+
latest.current = collections;
|
|
27
|
+
const resolver = useMemo(
|
|
28
|
+
() => ({
|
|
29
|
+
get: (name) => latest.current?.[name],
|
|
30
|
+
// Read through the same ref as `get`, so a registry passed as an inline
|
|
31
|
+
// object stays current without giving the resolver a new identity.
|
|
32
|
+
names: () => Object.keys(latest.current ?? {})
|
|
33
|
+
}),
|
|
34
|
+
[]
|
|
35
|
+
);
|
|
36
|
+
return /* @__PURE__ */ jsx(CollectionOptionsContext.Provider, { value: resolver, children });
|
|
37
|
+
}
|
|
38
|
+
function TalaDBProvider(props) {
|
|
39
|
+
if ("db" in props && props.db) {
|
|
40
|
+
return /* @__PURE__ */ jsx(TalaDBContext.Provider, { value: props.db, children: /* @__PURE__ */ jsx(CollectionOptionsProvider, { collections: props.collections, children: props.children }) });
|
|
41
|
+
}
|
|
42
|
+
return /* @__PURE__ */ jsx(NamedProvider, { ...props });
|
|
43
|
+
}
|
|
44
|
+
function NamedProvider({
|
|
45
|
+
name,
|
|
46
|
+
options,
|
|
47
|
+
fallback = null,
|
|
48
|
+
collections,
|
|
49
|
+
children
|
|
50
|
+
}) {
|
|
51
|
+
const [db, setDb] = useState(null);
|
|
52
|
+
const [error, setError] = useState(null);
|
|
53
|
+
const optionsKey = JSON.stringify(options ?? null);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
setError(null);
|
|
56
|
+
let cancelled = false;
|
|
57
|
+
let opened = null;
|
|
58
|
+
import("taladb").then(({ openDB }) => openDB(name, options)).then((instance) => {
|
|
59
|
+
if (cancelled) {
|
|
60
|
+
void instance.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
opened = instance;
|
|
64
|
+
setDb(instance);
|
|
65
|
+
}).catch((e) => {
|
|
66
|
+
if (!cancelled) setError(e);
|
|
67
|
+
});
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
if (opened) void opened.close();
|
|
71
|
+
setDb(null);
|
|
72
|
+
};
|
|
73
|
+
}, [name, optionsKey]);
|
|
74
|
+
if (error !== null) throw error;
|
|
75
|
+
if (db === null) return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
76
|
+
return /* @__PURE__ */ jsx(TalaDBContext.Provider, { value: db, children: /* @__PURE__ */ jsx(CollectionOptionsProvider, { collections, children }) });
|
|
77
|
+
}
|
|
78
|
+
function useTalaDB() {
|
|
79
|
+
const db = useContext(TalaDBContext);
|
|
80
|
+
if (db === null) {
|
|
81
|
+
throw new Error('useTalaDB must be used inside <TalaDBProvider db={...}> or <TalaDBProvider name="...">');
|
|
82
|
+
}
|
|
83
|
+
return db;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/useCollection.ts
|
|
87
|
+
import { useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
88
|
+
function useCollection(name, options) {
|
|
89
|
+
const db = useTalaDB();
|
|
90
|
+
const registry = useCollectionOptions();
|
|
91
|
+
const explicit = useRef2(options);
|
|
92
|
+
explicit.current = options;
|
|
93
|
+
return useMemo2(
|
|
94
|
+
() => db.collection(name, explicit.current ?? registry.get(name)),
|
|
95
|
+
[db, name, registry]
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/useFind.ts
|
|
100
|
+
import { useCallback, useRef as useRef3, useSyncExternalStore } from "react";
|
|
101
|
+
function useFind(collection, filter) {
|
|
102
|
+
const snapshotRef = useRef3({ data: [], loading: true, error: null });
|
|
103
|
+
const filterKey = JSON.stringify(filter ?? null);
|
|
104
|
+
const subscribe = useCallback(
|
|
105
|
+
(notify) => {
|
|
106
|
+
snapshotRef.current = { data: snapshotRef.current.data, loading: true, error: null };
|
|
107
|
+
return collection.subscribe(filter ?? {}, (docs) => {
|
|
108
|
+
snapshotRef.current = { data: docs, loading: false, error: null };
|
|
109
|
+
notify();
|
|
110
|
+
}, (error) => {
|
|
111
|
+
snapshotRef.current = { ...snapshotRef.current, loading: false, error };
|
|
112
|
+
notify();
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
// filterKey captures the serialised filter; collection is the identity dep.
|
|
116
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
117
|
+
[collection, filterKey]
|
|
118
|
+
);
|
|
119
|
+
const getSnapshot = useCallback(() => snapshotRef.current, []);
|
|
120
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
useCollectionOptions,
|
|
125
|
+
TalaDBProvider,
|
|
126
|
+
useTalaDB,
|
|
127
|
+
useCollection,
|
|
128
|
+
useFind
|
|
129
|
+
};
|