reactjrx 1.29.2 → 1.30.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/dist/index.cjs CHANGED
@@ -101,17 +101,23 @@ const useConstant = (fn) => {
101
101
  return ref;
102
102
  };
103
103
  const useSubject = ({
104
- onBeforeComplete
104
+ onBeforeComplete,
105
+ completeOnUnmount = true
105
106
  } = {}) => {
106
107
  const subject = useConstant(() => new rxjs.Subject());
107
108
  const completed = react.useRef(false);
108
109
  const onBeforeCompleteRef = useLiveRef(onBeforeComplete);
110
+ const completeOnUnmountRef = useLiveRef(completeOnUnmount);
109
111
  react.useEffect(() => {
110
112
  if (completed.current) {
111
113
  subject.current = new rxjs.Subject();
112
114
  completed.current = false;
113
115
  }
114
116
  return () => {
117
+ if (!completeOnUnmountRef.current) {
118
+ completed.current = true;
119
+ return;
120
+ }
115
121
  if (!completed.current) {
116
122
  if (onBeforeCompleteRef.current != null)
117
123
  onBeforeCompleteRef.current();
@@ -435,9 +441,40 @@ const useBehaviorSubject = (state) => {
435
441
  }, []);
436
442
  return subject;
437
443
  };
444
+ function shallowEqual(objA, objB) {
445
+ if (objA === null || objA === void 0 || objB === null || objB === void 0) {
446
+ return objA === objB;
447
+ }
448
+ if (typeof objA !== "object" || typeof objB !== "object") {
449
+ return objA === objB;
450
+ }
451
+ if (objA.constructor !== objB.constructor) {
452
+ return false;
453
+ }
454
+ const keysA = Object.keys(objA);
455
+ const keysB = Object.keys(objB);
456
+ if (keysA.length !== keysB.length) {
457
+ return false;
458
+ }
459
+ for (const key of keysA) {
460
+ if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
461
+ return false;
462
+ }
463
+ }
464
+ return true;
465
+ }
438
466
  function useMutation(query, mapOperatorOrOptions, options = {}) {
439
467
  const queryRef = useLiveRef(query);
440
468
  const triggerSubject = useSubject();
469
+ const resetSubject = useSubject({
470
+ /**
471
+ * @important
472
+ * Because mutation can still run after unmount, the user might
473
+ * want to use reset for whatever reason. We will only manually complete
474
+ * this subject whenever the main query hook finalize.
475
+ */
476
+ completeOnUnmount: false
477
+ });
441
478
  const optionsRef = useLiveRef(
442
479
  typeof mapOperatorOrOptions === "object" ? mapOperatorOrOptions : options
443
480
  );
@@ -449,55 +486,81 @@ function useMutation(query, mapOperatorOrOptions, options = {}) {
449
486
  const mapOperator = typeof mapOperatorOrOptions === "string" ? mapOperatorOrOptions : "merge";
450
487
  react.useEffect(() => {
451
488
  const switchOperator = mapOperator === "concat" ? rxjs.concatMap : mapOperator === "switch" ? rxjs.switchMap : rxjs.mergeMap;
452
- const subscription = triggerSubject.current.pipe(
453
- switchOperator((args) => {
454
- const isLastMutationCalled = triggerSubject.current.pipe(
455
- rxjs.take(1),
456
- rxjs.map(() => mapOperator === "concat"),
457
- rxjs.startWith(true)
458
- );
459
- data$.current.next({
460
- ...data$.current.getValue(),
461
- status: "loading"
462
- });
463
- return rxjs.combineLatest([
464
- rxjs.defer(() => rxjs.from(queryRef.current(args))).pipe(
465
- querx(optionsRef.current),
466
- rxjs.first(),
467
- rxjs.map((data) => ({ data, isError: false })),
468
- rxjs.catchError((error) => {
469
- if (optionsRef.current.onError != null) {
470
- optionsRef.current.onError(error);
471
- }
472
- return rxjs.of({ data: error, isError: true });
473
- })
474
- ),
475
- isLastMutationCalled
476
- ]).pipe(
477
- rxjs.tap(([{ data, isError }, isLastMutationCalled2]) => {
478
- if (!isError) {
479
- if (optionsRef.current.onSuccess != null)
480
- optionsRef.current.onSuccess(data);
481
- }
482
- if (isLastMutationCalled2) {
483
- data$.current.next({
484
- ...data$.current.getValue(),
485
- ...isError ? {
486
- status: "error",
487
- error: data,
488
- data: void 0
489
- } : {
490
- status: "success",
491
- error: void 0,
492
- data
493
- }
494
- });
495
- }
489
+ const subscription = rxjs.merge(
490
+ resetSubject.current.pipe(
491
+ rxjs.map(
492
+ () => ({
493
+ status: "idle",
494
+ data: void 0,
495
+ error: void 0
496
496
  })
497
- );
498
- }),
499
- optionsRef.current.hooks ?? rxjs.identity
500
- ).subscribe();
497
+ )
498
+ ),
499
+ triggerSubject.current.pipe(
500
+ switchOperator((args) => {
501
+ const isLastMutationCalled = triggerSubject.current.pipe(
502
+ rxjs.take(1),
503
+ rxjs.map(() => mapOperator === "concat"),
504
+ rxjs.startWith(true)
505
+ );
506
+ return rxjs.merge(
507
+ rxjs.of({
508
+ status: "loading"
509
+ }),
510
+ rxjs.combineLatest([
511
+ rxjs.defer(() => rxjs.from(queryRef.current(args))).pipe(
512
+ querx(optionsRef.current),
513
+ rxjs.first(),
514
+ rxjs.map((data) => ({ data, isError: false })),
515
+ rxjs.catchError((error) => {
516
+ if (optionsRef.current.onError != null) {
517
+ optionsRef.current.onError(error);
518
+ }
519
+ return rxjs.of({ data: error, isError: true });
520
+ })
521
+ ),
522
+ isLastMutationCalled
523
+ ]).pipe(
524
+ rxjs.map(([{ data, isError }, isLastMutationCalled2]) => {
525
+ if (!isError) {
526
+ if (optionsRef.current.onSuccess != null)
527
+ optionsRef.current.onSuccess(data);
528
+ }
529
+ if (isLastMutationCalled2) {
530
+ return isError ? {
531
+ status: "error",
532
+ error: data,
533
+ data: void 0
534
+ } : {
535
+ status: "success",
536
+ error: void 0,
537
+ data
538
+ };
539
+ }
540
+ return void 0;
541
+ }),
542
+ rxjs.takeUntil(resetSubject.current)
543
+ )
544
+ );
545
+ }),
546
+ optionsRef.current.triggerHook ?? rxjs.identity,
547
+ rxjs.finalize(() => {
548
+ resetSubject.current.complete();
549
+ })
550
+ )
551
+ ).pipe(
552
+ /**
553
+ * @important
554
+ * state update optimization
555
+ */
556
+ rxjs.distinctUntilChanged(shallowEqual),
557
+ rxjs.filter((state) => !!state && !!Object.keys(state).length)
558
+ ).subscribe((state) => {
559
+ data$.current.next({
560
+ ...data$.current.getValue(),
561
+ ...state
562
+ });
563
+ });
501
564
  return () => {
502
565
  if (optionsRef.current.cancelOnUnMount) {
503
566
  subscription.unsubscribe();
@@ -514,31 +577,12 @@ function useMutation(query, mapOperatorOrOptions, options = {}) {
514
577
  const mutate = react.useCallback((arg) => {
515
578
  triggerSubject.current.next(arg);
516
579
  }, []);
517
- return { ...result, isLoading: result.status === "loading", mutate };
580
+ const reset = react.useCallback(() => {
581
+ resetSubject.current.next();
582
+ }, []);
583
+ return { ...result, isLoading: result.status === "loading", mutate, reset };
518
584
  }
519
585
  const arrayEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
520
- function shallowEqual(objA, objB) {
521
- if (objA === null || objA === void 0 || objB === null || objB === void 0) {
522
- return objA === objB;
523
- }
524
- if (typeof objA !== "object" || typeof objB !== "object") {
525
- return objA === objB;
526
- }
527
- if (objA.constructor !== objB.constructor) {
528
- return false;
529
- }
530
- const keysA = Object.keys(objA);
531
- const keysB = Object.keys(objB);
532
- if (keysA.length !== keysB.length) {
533
- return false;
534
- }
535
- for (const key of keysA) {
536
- if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
537
- return false;
538
- }
539
- }
540
- return true;
541
- }
542
586
  const useCreateCacheStore = () => {
543
587
  const cacheStore = useBehaviorSubject({});
544
588
  useSubscribe(
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useRef, useMemo, useCallback, useSyncExternalStore, useEffect, createContext, memo, useContext, useState } from "react";
2
- import { distinctUntilChanged, tap, finalize, catchError, EMPTY, Subject, identity, BehaviorSubject, of, zip, from, map, merge, throttleTime, switchMap, defer, iif, timer, throwError, take, startWith, combineLatest, first, concatMap as concatMap$1, mergeMap, skip, interval, withLatestFrom, shareReplay, repeat, filter, share, takeUntil, retry } from "rxjs";
2
+ import { distinctUntilChanged, tap, finalize, catchError, EMPTY, Subject, identity, BehaviorSubject, of, zip, from, map, merge, throttleTime, switchMap, defer, iif, timer, throwError, take, startWith, combineLatest, first, takeUntil, filter, concatMap as concatMap$1, mergeMap, skip, interval, withLatestFrom, shareReplay, repeat, share, retry } from "rxjs";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { retryWhen, concatMap, tap as tap$1 } from "rxjs/operators";
5
5
  const useLiveRef = (value) => {
@@ -99,17 +99,23 @@ const useConstant = (fn) => {
99
99
  return ref;
100
100
  };
101
101
  const useSubject = ({
102
- onBeforeComplete
102
+ onBeforeComplete,
103
+ completeOnUnmount = true
103
104
  } = {}) => {
104
105
  const subject = useConstant(() => new Subject());
105
106
  const completed = useRef(false);
106
107
  const onBeforeCompleteRef = useLiveRef(onBeforeComplete);
108
+ const completeOnUnmountRef = useLiveRef(completeOnUnmount);
107
109
  useEffect(() => {
108
110
  if (completed.current) {
109
111
  subject.current = new Subject();
110
112
  completed.current = false;
111
113
  }
112
114
  return () => {
115
+ if (!completeOnUnmountRef.current) {
116
+ completed.current = true;
117
+ return;
118
+ }
113
119
  if (!completed.current) {
114
120
  if (onBeforeCompleteRef.current != null)
115
121
  onBeforeCompleteRef.current();
@@ -433,9 +439,40 @@ const useBehaviorSubject = (state) => {
433
439
  }, []);
434
440
  return subject;
435
441
  };
442
+ function shallowEqual(objA, objB) {
443
+ if (objA === null || objA === void 0 || objB === null || objB === void 0) {
444
+ return objA === objB;
445
+ }
446
+ if (typeof objA !== "object" || typeof objB !== "object") {
447
+ return objA === objB;
448
+ }
449
+ if (objA.constructor !== objB.constructor) {
450
+ return false;
451
+ }
452
+ const keysA = Object.keys(objA);
453
+ const keysB = Object.keys(objB);
454
+ if (keysA.length !== keysB.length) {
455
+ return false;
456
+ }
457
+ for (const key of keysA) {
458
+ if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
459
+ return false;
460
+ }
461
+ }
462
+ return true;
463
+ }
436
464
  function useMutation(query, mapOperatorOrOptions, options = {}) {
437
465
  const queryRef = useLiveRef(query);
438
466
  const triggerSubject = useSubject();
467
+ const resetSubject = useSubject({
468
+ /**
469
+ * @important
470
+ * Because mutation can still run after unmount, the user might
471
+ * want to use reset for whatever reason. We will only manually complete
472
+ * this subject whenever the main query hook finalize.
473
+ */
474
+ completeOnUnmount: false
475
+ });
439
476
  const optionsRef = useLiveRef(
440
477
  typeof mapOperatorOrOptions === "object" ? mapOperatorOrOptions : options
441
478
  );
@@ -447,55 +484,81 @@ function useMutation(query, mapOperatorOrOptions, options = {}) {
447
484
  const mapOperator = typeof mapOperatorOrOptions === "string" ? mapOperatorOrOptions : "merge";
448
485
  useEffect(() => {
449
486
  const switchOperator = mapOperator === "concat" ? concatMap$1 : mapOperator === "switch" ? switchMap : mergeMap;
450
- const subscription = triggerSubject.current.pipe(
451
- switchOperator((args) => {
452
- const isLastMutationCalled = triggerSubject.current.pipe(
453
- take(1),
454
- map(() => mapOperator === "concat"),
455
- startWith(true)
456
- );
457
- data$.current.next({
458
- ...data$.current.getValue(),
459
- status: "loading"
460
- });
461
- return combineLatest([
462
- defer(() => from(queryRef.current(args))).pipe(
463
- querx(optionsRef.current),
464
- first(),
465
- map((data) => ({ data, isError: false })),
466
- catchError((error) => {
467
- if (optionsRef.current.onError != null) {
468
- optionsRef.current.onError(error);
469
- }
470
- return of({ data: error, isError: true });
471
- })
472
- ),
473
- isLastMutationCalled
474
- ]).pipe(
475
- tap(([{ data, isError }, isLastMutationCalled2]) => {
476
- if (!isError) {
477
- if (optionsRef.current.onSuccess != null)
478
- optionsRef.current.onSuccess(data);
479
- }
480
- if (isLastMutationCalled2) {
481
- data$.current.next({
482
- ...data$.current.getValue(),
483
- ...isError ? {
484
- status: "error",
485
- error: data,
486
- data: void 0
487
- } : {
488
- status: "success",
489
- error: void 0,
490
- data
491
- }
492
- });
493
- }
487
+ const subscription = merge(
488
+ resetSubject.current.pipe(
489
+ map(
490
+ () => ({
491
+ status: "idle",
492
+ data: void 0,
493
+ error: void 0
494
494
  })
495
- );
496
- }),
497
- optionsRef.current.hooks ?? identity
498
- ).subscribe();
495
+ )
496
+ ),
497
+ triggerSubject.current.pipe(
498
+ switchOperator((args) => {
499
+ const isLastMutationCalled = triggerSubject.current.pipe(
500
+ take(1),
501
+ map(() => mapOperator === "concat"),
502
+ startWith(true)
503
+ );
504
+ return merge(
505
+ of({
506
+ status: "loading"
507
+ }),
508
+ combineLatest([
509
+ defer(() => from(queryRef.current(args))).pipe(
510
+ querx(optionsRef.current),
511
+ first(),
512
+ map((data) => ({ data, isError: false })),
513
+ catchError((error) => {
514
+ if (optionsRef.current.onError != null) {
515
+ optionsRef.current.onError(error);
516
+ }
517
+ return of({ data: error, isError: true });
518
+ })
519
+ ),
520
+ isLastMutationCalled
521
+ ]).pipe(
522
+ map(([{ data, isError }, isLastMutationCalled2]) => {
523
+ if (!isError) {
524
+ if (optionsRef.current.onSuccess != null)
525
+ optionsRef.current.onSuccess(data);
526
+ }
527
+ if (isLastMutationCalled2) {
528
+ return isError ? {
529
+ status: "error",
530
+ error: data,
531
+ data: void 0
532
+ } : {
533
+ status: "success",
534
+ error: void 0,
535
+ data
536
+ };
537
+ }
538
+ return void 0;
539
+ }),
540
+ takeUntil(resetSubject.current)
541
+ )
542
+ );
543
+ }),
544
+ optionsRef.current.triggerHook ?? identity,
545
+ finalize(() => {
546
+ resetSubject.current.complete();
547
+ })
548
+ )
549
+ ).pipe(
550
+ /**
551
+ * @important
552
+ * state update optimization
553
+ */
554
+ distinctUntilChanged(shallowEqual),
555
+ filter((state) => !!state && !!Object.keys(state).length)
556
+ ).subscribe((state) => {
557
+ data$.current.next({
558
+ ...data$.current.getValue(),
559
+ ...state
560
+ });
561
+ });
499
562
  return () => {
500
563
  if (optionsRef.current.cancelOnUnMount) {
501
564
  subscription.unsubscribe();
@@ -512,31 +575,12 @@ function useMutation(query, mapOperatorOrOptions, options = {}) {
512
575
  const mutate = useCallback((arg) => {
513
576
  triggerSubject.current.next(arg);
514
577
  }, []);
515
- return { ...result, isLoading: result.status === "loading", mutate };
578
+ const reset = useCallback(() => {
579
+ resetSubject.current.next();
580
+ }, []);
581
+ return { ...result, isLoading: result.status === "loading", mutate, reset };
516
582
  }
517
583
  const arrayEqual = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
518
- function shallowEqual(objA, objB) {
519
- if (objA === null || objA === void 0 || objB === null || objB === void 0) {
520
- return objA === objB;
521
- }
522
- if (typeof objA !== "object" || typeof objB !== "object") {
523
- return objA === objB;
524
- }
525
- if (objA.constructor !== objB.constructor) {
526
- return false;
527
- }
528
- const keysA = Object.keys(objA);
529
- const keysB = Object.keys(objB);
530
- if (keysA.length !== keysB.length) {
531
- return false;
532
- }
533
- for (const key of keysA) {
534
- if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) {
535
- return false;
536
- }
537
- }
538
- return true;
539
- }
540
584
  const useCreateCacheStore = () => {
541
585
  const cacheStore = useBehaviorSubject({});
542
586
  useSubscribe(
@@ -3,8 +3,9 @@ import { Subject } from "rxjs";
3
3
  * @see
4
4
  * useBehaviorSubject
5
5
  */
6
- export declare const useSubject: <S>({ onBeforeComplete }?: {
6
+ export declare const useSubject: <S>({ onBeforeComplete, completeOnUnmount }?: {
7
7
  onBeforeComplete?: (() => void) | undefined;
8
+ completeOnUnmount?: boolean | undefined;
8
9
  }) => {
9
10
  current: Subject<S>;
10
11
  };
@@ -1,4 +1,9 @@
1
1
  import { type MonoTypeOperatorFunction, type Observable } from "rxjs";
2
+ interface QueryState<R> {
3
+ data: R | undefined;
4
+ status: "idle" | "loading" | "error" | "success";
5
+ error: unknown;
6
+ }
2
7
  export interface MutationOptions<R> {
3
8
  retry?: false | number | ((attempt: number, error: unknown) => boolean);
4
9
  /**
@@ -25,9 +30,10 @@ export interface MutationOptions<R> {
25
30
  */
26
31
  cancelOnUnMount?: boolean;
27
32
  /**
28
- * Only use for debugging
33
+ * Only use for debugging.
34
+ * It is not the main subscription hook, only the one following the trigger.
29
35
  */
30
- hooks?: MonoTypeOperatorFunction<any>;
36
+ triggerHook?: MonoTypeOperatorFunction<Partial<QueryState<R>> | undefined>;
31
37
  }
32
38
  interface Result<A, R> {
33
39
  status: "idle" | "loading" | "error" | "success";
@@ -49,6 +55,7 @@ interface Result<A, R> {
49
55
  */
50
56
  error: unknown | undefined;
51
57
  mutate: (args: A) => void;
58
+ reset: () => void;
52
59
  }
53
60
  /**
54
61
  * The default value `merge` is suitable for most use case.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactjrx",
3
3
  "private": false,
4
- "version": "1.29.2",
4
+ "version": "1.30.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist"
@@ -57,9 +57,7 @@
57
57
  "typescript": "^5.0.4",
58
58
  "vite": "^4.2.1",
59
59
  "vite-plugin-dts": "^2.2.0",
60
- "vitest": "^0.31.0"
61
- },
62
- "dependencies": {
60
+ "vitest": "^0.31.0",
63
61
  "@testing-library/react": "^14.0.0",
64
62
  "@types/node": "^18.15.11"
65
63
  },