@livequery/react 2.0.136 → 2.0.137
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 +95 -0
- 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
|
@@ -193,6 +193,98 @@ Behavior notes:
|
|
|
193
193
|
- If the source is `undefined`, the hook returns the default value, or `undefined` if no default was provided.
|
|
194
194
|
- Reading `.value` or `.getValue()` manually in render is not a replacement for `useObservable()` because it will not subscribe the component to future emissions.
|
|
195
195
|
|
|
196
|
+
## Rendering a Collection (Mandatory Pattern)
|
|
197
|
+
|
|
198
|
+
`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.
|
|
199
|
+
|
|
200
|
+
**How it works:**
|
|
201
|
+
|
|
202
|
+
- The outer `BehaviorSubject` emits a new array only when items are added, removed, or reordered.
|
|
203
|
+
- Each element in the array is itself a `BehaviorSubject<T>` that emits whenever that specific item's fields change.
|
|
204
|
+
- 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.
|
|
205
|
+
|
|
206
|
+
This means correct rendering requires three separate component layers:
|
|
207
|
+
|
|
208
|
+
### Rule 1 — Subscribe to the items array in the parent
|
|
209
|
+
|
|
210
|
+
```tsx
|
|
211
|
+
const items = useObservable(collection.items, [])
|
|
212
|
+
// items = BehaviorSubject<T>[]
|
|
213
|
+
// Re-renders only when item count or order changes
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Rule 2 — Render each item in its own component
|
|
217
|
+
|
|
218
|
+
Pass the `BehaviorSubject<T>` as a prop and call `useObservable` inside the child. Field changes re-render only that child.
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
function TodoItem({ item$ }: { item$: BehaviorSubject<Todo> }) {
|
|
222
|
+
const item = useObservable(item$)
|
|
223
|
+
return <li>{item.title}</li>
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Rule 3 — Render loading state in its own component
|
|
228
|
+
|
|
229
|
+
`collection.loading` is also a `BehaviorSubject<boolean>`. Place it in a separate component so toggling loading does not re-render the item list.
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
function TodoLoading({ loading$ }: { loading$: BehaviorSubject<boolean> }) {
|
|
233
|
+
const loading = useObservable(loading$)
|
|
234
|
+
if (!loading) return null
|
|
235
|
+
return <p>Loading...</p>
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Full example
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
import { useEffect } from 'react'
|
|
243
|
+
import { BehaviorSubject } from 'rxjs'
|
|
244
|
+
import { useCollection, useObservable } from '@livequery/react'
|
|
245
|
+
|
|
246
|
+
type Todo = { _id: string; title: string; done: boolean }
|
|
247
|
+
|
|
248
|
+
function TodoLoading({ loading$ }: { loading$: BehaviorSubject<boolean> }) {
|
|
249
|
+
const loading = useObservable(loading$)
|
|
250
|
+
if (!loading) return null
|
|
251
|
+
return <p>Loading...</p>
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function TodoItem({ item$ }: { item$: BehaviorSubject<Todo> }) {
|
|
255
|
+
const item = useObservable(item$)
|
|
256
|
+
return <li>{item.title}</li>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function TodoList() {
|
|
260
|
+
const collection = useCollection<Todo>('todos')
|
|
261
|
+
const items = useObservable(collection.items, [])
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
collection.query()
|
|
265
|
+
}, [collection])
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<>
|
|
269
|
+
<TodoLoading loading$={collection.loading} />
|
|
270
|
+
<ul>
|
|
271
|
+
{items.map((item$) => (
|
|
272
|
+
<TodoItem key={item$.getValue()._id} item$={item$} />
|
|
273
|
+
))}
|
|
274
|
+
</ul>
|
|
275
|
+
</>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Why this matters:**
|
|
281
|
+
|
|
282
|
+
- 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.
|
|
283
|
+
- 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.
|
|
284
|
+
- Following all three rules gives true per-item granularity: only the component that owns a changed field re-renders.
|
|
285
|
+
|
|
286
|
+
> **Never** flatten `collection.items` by calling `useObservable` on each element inside the parent map. Always delegate to a child component.
|
|
287
|
+
|
|
196
288
|
## `useAction`
|
|
197
289
|
|
|
198
290
|
`useAction(fn, options)` wraps an async function and attaches action state to the returned callable.
|
|
@@ -283,6 +375,9 @@ Behavior notes:
|
|
|
283
375
|
- Passing changing `useCollection()` options and expecting the existing collection instance to rebuild.
|
|
284
376
|
- Using `useDocument()` when you need error state or collection methods.
|
|
285
377
|
- Importing APIs not listed in the `Exports` section.
|
|
378
|
+
- 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.
|
|
379
|
+
- Observing `collection.loading` in the same component as the item list — loading state changes then re-render the full list.
|
|
380
|
+
- 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
381
|
|
|
287
382
|
## Build
|
|
288
383
|
|
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"}
|