@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 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
 
@@ -1,6 +1,7 @@
1
1
  import { Observable, BehaviorSubject } from "rxjs";
2
2
  export type MaybeFunction<T> = T | (() => T);
3
- type ObservableSource<T> = MaybeFunction<BehaviorSubject<T> | Observable<T>> | undefined;
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,EAAc,MAAM,MAAM,CAAC;AAG/D,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;AAE5C,KAAK,gBAAgB,CAAC,CAAC,IAAI,aAAa,CAAC,eAAe,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,CAAA;AAExF,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"}
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"}
@@ -1,22 +1,28 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { Observable, BehaviorSubject, tap, EMPTY } from "rxjs";
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 prev = useRef(o);
6
- const source = o || EMPTY;
7
- const isBehaviorSubject = typeof o == 'object' && typeof source.getValue === 'function';
8
- const dfv = isBehaviorSubject ? source.getValue() : default_value;
9
- const [v, s] = useState(dfv);
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 !== o;
12
- prev.current = o;
13
- try {
14
- const subscription = source.pipe(skip(isBehaviorSubject && !diff ? 1 : 0), tap(s)).subscribe();
15
- return () => {
16
- subscription.unsubscribe();
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,KAAK,EAAE,MAAM,MAAM,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAUtC,MAAM,UAAU,aAAa,CAAI,CAAmB,EAAE,aAAiB;IACnE,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IACtB,MAAM,MAAM,GAAG,CAA8B,IAAI,KAAK,CAAA;IACtD,MAAM,iBAAiB,GAAE,OAAO,CAAC,IAAI,QAAQ,IAAK,OAAO,MAAM,CAAC,QAAQ,KAAK,UAAU,CAAA;IACvF,MAAM,GAAG,GAAG,iBAAiB,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,aAAa,CAAA;IACjE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAgB,GAAG,CAAC,CAAA;IAC3C,SAAS,CAAC,GAAG,EAAE;QACX,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,KAAK,CAAC,CAAA;QAC/B,IAAI,CAAC,OAAO,GAAG,CAAC,CAAA;QAChB,IAAI,CAAC;YACD,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAC5B,IAAI,CAAC,iBAAiB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EACxC,GAAG,CAAC,CAAC,CAAC,CACT,CAAC,SAAS,EAAE,CAAA;YACb,OAAO,GAAG,EAAE;gBACR,YAAY,CAAC,WAAW,EAAE,CAAA;YAC9B,CAAC,CAAA;QACL,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;IACnB,CAAC,EAAE,OAAO,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAEtC,OAAO,CAAC,CAAA;AACZ,CAAC"}
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"}
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "url": "https://github.com/livequery/react"
5
5
  },
6
6
  "type": "module",
7
- "version": "2.0.136",
7
+ "version": "2.0.137",
8
8
  "description": "",
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",