@livequery/react 2.0.136 → 2.0.138
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 +94 -5
- package/dist/useObservable.d.ts +2 -1
- package/dist/useObservable.d.ts.map +1 -1
- package/dist/useObservable.js +21 -15
- package/dist/useObservable.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,7 +91,6 @@ The hook must be used under a matching provider. If it is called outside the pro
|
|
|
91
91
|
Use it when a component needs the full collection API: reactive state plus methods such as querying or mutations.
|
|
92
92
|
|
|
93
93
|
```tsx
|
|
94
|
-
import { useEffect } from 'react'
|
|
95
94
|
import { useCollection, useObservable } from '@livequery/react'
|
|
96
95
|
|
|
97
96
|
type Todo = {
|
|
@@ -101,15 +100,12 @@ type Todo = {
|
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
export function TodoList() {
|
|
103
|
+
// lazy: false — collection queries automatically on initialization
|
|
104
104
|
const collection = useCollection<Todo>('todos', { lazy: false })
|
|
105
105
|
const items = useObservable(collection.items, [])
|
|
106
106
|
const loading = useObservable(collection.loading, false)
|
|
107
107
|
const error = useObservable(collection.error)
|
|
108
108
|
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
collection.query()
|
|
111
|
-
}, [collection])
|
|
112
|
-
|
|
113
109
|
if (loading) return <p>Loading...</p>
|
|
114
110
|
if (error) return <p>Could not load todos.</p>
|
|
115
111
|
|
|
@@ -123,6 +119,8 @@ export function TodoList() {
|
|
|
123
119
|
}
|
|
124
120
|
```
|
|
125
121
|
|
|
122
|
+
When `lazy: false`, the collection queries automatically when initialized — no `useEffect` or manual `collection.query()` call is needed. Use `lazy: true` (the default) when you need to control when the query fires, such as after user interaction or after other async setup completes.
|
|
123
|
+
|
|
126
124
|
Behavior notes:
|
|
127
125
|
|
|
128
126
|
- `ref` may be `undefined`, `null`, `false`, or an empty string. Falsy refs skip initialization.
|
|
@@ -193,6 +191,94 @@ Behavior notes:
|
|
|
193
191
|
- If the source is `undefined`, the hook returns the default value, or `undefined` if no default was provided.
|
|
194
192
|
- Reading `.value` or `.getValue()` manually in render is not a replacement for `useObservable()` because it will not subscribe the component to future emissions.
|
|
195
193
|
|
|
194
|
+
## Rendering a Collection (Mandatory Pattern)
|
|
195
|
+
|
|
196
|
+
`collection.items` is a `BehaviorSubject<BehaviorSubject<T>[]>`. This two-level structure is intentional and must be respected to achieve both realtime updates and high render performance.
|
|
197
|
+
|
|
198
|
+
**How it works:**
|
|
199
|
+
|
|
200
|
+
- The outer `BehaviorSubject` emits a new array only when items are added, removed, or reordered.
|
|
201
|
+
- Each element in the array is itself a `BehaviorSubject<T>` that emits whenever that specific item's fields change.
|
|
202
|
+
- A field update on one item emits only that item's inner subject — the outer array does not change and the parent list does not re-render.
|
|
203
|
+
|
|
204
|
+
This means correct rendering requires three separate component layers:
|
|
205
|
+
|
|
206
|
+
### Rule 1 — Subscribe to the items array in the parent
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
const items = useObservable(collection.items, [])
|
|
210
|
+
// items = BehaviorSubject<T>[]
|
|
211
|
+
// Re-renders only when item count or order changes
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Rule 2 — Render each item in its own component
|
|
215
|
+
|
|
216
|
+
Pass the `BehaviorSubject<T>` as a prop and call `useObservable` inside the child. Field changes re-render only that child.
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
function TodoItem({ item$ }: { item$: BehaviorSubject<Todo> }) {
|
|
220
|
+
const item = useObservable(item$)
|
|
221
|
+
return <li>{item.title}</li>
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Rule 3 — Render loading state in its own component
|
|
226
|
+
|
|
227
|
+
`collection.loading` is also a `BehaviorSubject<boolean>`. Place it in a separate component so toggling loading does not re-render the item list.
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
function TodoLoading({ loading$ }: { loading$: BehaviorSubject<boolean> }) {
|
|
231
|
+
const loading = useObservable(loading$)
|
|
232
|
+
if (!loading) return null
|
|
233
|
+
return <p>Loading...</p>
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Full example
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
import { BehaviorSubject } from 'rxjs'
|
|
241
|
+
import { useCollection, useObservable } from '@livequery/react'
|
|
242
|
+
|
|
243
|
+
type Todo = { _id: string; title: string; done: boolean }
|
|
244
|
+
|
|
245
|
+
function TodoLoading({ loading$ }: { loading$: BehaviorSubject<boolean> }) {
|
|
246
|
+
const loading = useObservable(loading$)
|
|
247
|
+
if (!loading) return null
|
|
248
|
+
return <p>Loading...</p>
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function TodoItem({ item$ }: { item$: BehaviorSubject<Todo> }) {
|
|
252
|
+
const item = useObservable(item$)
|
|
253
|
+
return <li>{item.title}</li>
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function TodoList() {
|
|
257
|
+
// lazy: false — no need to call collection.query() manually
|
|
258
|
+
const collection = useCollection<Todo>('todos', { lazy: false })
|
|
259
|
+
const items = useObservable(collection.items, [])
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
<TodoLoading loading$={collection.loading} />
|
|
264
|
+
<ul>
|
|
265
|
+
{items.map((item$) => (
|
|
266
|
+
<TodoItem key={item$.getValue()._id} item$={item$} />
|
|
267
|
+
))}
|
|
268
|
+
</ul>
|
|
269
|
+
</>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Why this matters:**
|
|
275
|
+
|
|
276
|
+
- Putting `useObservable(collection.loading)` and `useObservable(collection.items)` in the same parent component means every loading toggle re-renders the entire list, even when nothing changed.
|
|
277
|
+
- Calling `useObservable(item$)` inside the parent map instead of a child component means every field change on any single item re-renders the whole list.
|
|
278
|
+
- Following all three rules gives true per-item granularity: only the component that owns a changed field re-renders.
|
|
279
|
+
|
|
280
|
+
> **Never** flatten `collection.items` by calling `useObservable` on each element inside the parent map. Always delegate to a child component.
|
|
281
|
+
|
|
196
282
|
## `useAction`
|
|
197
283
|
|
|
198
284
|
`useAction(fn, options)` wraps an async function and attaches action state to the returned callable.
|
|
@@ -283,6 +369,9 @@ Behavior notes:
|
|
|
283
369
|
- Passing changing `useCollection()` options and expecting the existing collection instance to rebuild.
|
|
284
370
|
- Using `useDocument()` when you need error state or collection methods.
|
|
285
371
|
- Importing APIs not listed in the `Exports` section.
|
|
372
|
+
- Calling `useObservable(item$)` for each item inside the parent `.map()` instead of delegating to a child component — this causes the entire list to re-render on every field change of any single item.
|
|
373
|
+
- Observing `collection.loading` in the same component as the item list — loading state changes then re-render the full list.
|
|
374
|
+
- Treating `collection.items` as a plain array — it is a `BehaviorSubject<BehaviorSubject<T>[]>` and must be observed at both levels to get correct realtime behavior.
|
|
286
375
|
|
|
287
376
|
## Build
|
|
288
377
|
|
package/dist/useObservable.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Observable, BehaviorSubject } from "rxjs";
|
|
2
2
|
export type MaybeFunction<T> = T | (() => T);
|
|
3
|
-
type
|
|
3
|
+
type Source<T> = BehaviorSubject<T> | Observable<T>;
|
|
4
|
+
type ObservableSource<T> = MaybeFunction<Source<T>> | undefined;
|
|
4
5
|
export declare function useObservable<T>(o: BehaviorSubject<T>): T;
|
|
5
6
|
export declare function useObservable<T>(o: ObservableSource<T>): T | undefined;
|
|
6
7
|
export declare function useObservable<T>(o: ObservableSource<T>, default_value: T): T;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObservable.d.ts","sourceRoot":"","sources":["../src/useObservable.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"useObservable.d.ts","sourceRoot":"","sources":["../src/useObservable.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAO,MAAM,MAAM,CAAC;AAGxD,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;AAE5C,KAAK,MAAM,CAAC,CAAC,IAAI,eAAe,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;AACnD,KAAK,gBAAgB,CAAC,CAAC,IAAI,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAA;AAU/D,wBAAgB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAC1D,wBAAgB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,CAAA;AACvE,wBAAgB,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,GAAG,CAAC,CAAA"}
|
package/dist/useObservable.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
-
import { Observable, BehaviorSubject, tap
|
|
2
|
+
import { Observable, BehaviorSubject, tap } from "rxjs";
|
|
3
3
|
import { skip } from "rxjs/operators";
|
|
4
|
+
const isBehaviorSubject = (source) => {
|
|
5
|
+
return typeof source?.getValue === 'function';
|
|
6
|
+
};
|
|
7
|
+
const hasPipe = (source) => {
|
|
8
|
+
return typeof source?.pipe === 'function';
|
|
9
|
+
};
|
|
4
10
|
export function useObservable(o, default_value) {
|
|
5
|
-
const
|
|
6
|
-
const source = o
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
11
|
+
const lazySource = useRef(undefined);
|
|
12
|
+
const source = typeof o === 'function'
|
|
13
|
+
? lazySource.current ?? (lazySource.current = o())
|
|
14
|
+
: o;
|
|
15
|
+
const prev = useRef(source);
|
|
16
|
+
const [v, s] = useState(() => isBehaviorSubject(source) ? source.getValue() : default_value);
|
|
10
17
|
useEffect(() => {
|
|
11
|
-
const diff = prev.current !==
|
|
12
|
-
prev.current =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
catch (e) { }
|
|
18
|
+
const diff = prev.current !== source;
|
|
19
|
+
prev.current = source;
|
|
20
|
+
if (!hasPipe(source))
|
|
21
|
+
return;
|
|
22
|
+
const subscription = source.pipe(skip(isBehaviorSubject(source) && !diff ? 1 : 0), tap(s)).subscribe();
|
|
23
|
+
return () => {
|
|
24
|
+
subscription.unsubscribe();
|
|
25
|
+
};
|
|
20
26
|
}, typeof o === 'function' ? [] : [o]);
|
|
21
27
|
return v;
|
|
22
28
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObservable.js","sourceRoot":"","sources":["../src/useObservable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,
|
|
1
|
+
{"version":3,"file":"useObservable.js","sourceRoot":"","sources":["../src/useObservable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAOtC,MAAM,iBAAiB,GAAG,CAAI,MAA6B,EAAgC,EAAE;IACzF,OAAO,OAAQ,MAAkD,EAAE,QAAQ,KAAK,UAAU,CAAA;AAC9F,CAAC,CAAA;AAED,MAAM,OAAO,GAAG,CAAI,MAA6B,EAAuB,EAAE;IACtE,OAAO,OAAQ,MAA6C,EAAE,IAAI,KAAK,UAAU,CAAA;AACrF,CAAC,CAAA;AAMD,MAAM,UAAU,aAAa,CAAI,CAAsB,EAAE,aAAiB;IACtE,MAAM,UAAU,GAAG,MAAM,CAAwB,SAAS,CAAC,CAAA;IAC3D,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,UAAU;QAClC,CAAC,CAAC,UAAU,CAAC,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;QAClD,CAAC,CAAC,CAAC,CAAA;IACP,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAC3B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAgB,GAAG,EAAE,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;IAE3G,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,KAAK,MAAM,CAAA;QACpC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAA;QAErB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,OAAM;QAE5B,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAC5B,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAChD,GAAG,CAAC,CAAC,CAAC,CACT,CAAC,SAAS,EAAE,CAAA;QACb,OAAO,GAAG,EAAE;YACR,YAAY,CAAC,WAAW,EAAE,CAAA;QAC9B,CAAC,CAAA;IACL,CAAC,EAAE,OAAO,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEtC,OAAO,CAAC,CAAA;AACZ,CAAC"}
|