@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 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
 
@@ -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.138",
8
8
  "description": "",
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",