Skip to content

Commit d5bbd0b

Browse files
committed
feat: 🎸 use-observable support deps
1 parent 1b55364 commit d5bbd0b

File tree

4 files changed

+101
-67
lines changed

4 files changed

+101
-67
lines changed

‎README.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ function CounterComponent() {
4141

4242
`useObservable` can take the initial value as the second parameter - `useObservable(source$, initialValue)`. If the source fires synchronously immediately (like in a `BehaviorSubject`), the value will be used as the initial value.
4343

44+
You can also pass a dependencies:
45+
46+
```tsx
47+
import { useObservable } from '@ngneat/react-rxjs';
48+
49+
const SomeComponent = ({ id }: { id: string }) => {
50+
const [state] = useObservable(getStream$(id), { deps: [id] })
51+
52+
return state;
53+
}
54+
```
55+
4456

4557
## useUntilDestroyed
4658

‎package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"lint": "nx run-many --target=lint --exclude=playground --all",
99
"build": "nx build react-rxjs",
1010
"c": "git-cz",
11-
"prepare": "husky install"
11+
"prepare": "husky install",
12+
"update": "nx migrate latest",
13+
"migrate": "nx migrate --run-migrations"
1214
},
1315
"private": true,
1416
"devDependencies": {
@@ -67,3 +69,4 @@
6769
]
6870
}
6971
}
72+
Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,51 @@ import { useObservable } from './use-observable';
22
import { renderHook, act } from '@testing-library/react-hooks';
33
import { interval, BehaviorSubject, of } from 'rxjs';
44
import { finalize, map } from 'rxjs/operators';
5+
import { useState } from 'react';
6+
import { render, fireEvent } from '@testing-library/react';
57

68
jest.useFakeTimers();
79

8-
describe('useObservable', () => {
9-
it('should support BehaviorSubject', () => {
10-
const query = new BehaviorSubject('init');
11-
const { result } = renderHook(() => useObservable(query));
12-
let [next] = result.current;
10+
const store = new BehaviorSubject('1');
1311

14-
expect(next).toBe('init');
12+
function getStream$(id: string) {
13+
return of(id);
14+
}
1515

16-
act(() => query.next('2'));
16+
const SomeComponent = ({ id }: { id: string }) => {
17+
const [state] = useObservable(getStream$(id), { deps: [id] })
1718

18-
[next] = result.current;
19-
expect(next).toBe('2');
20-
});
19+
return <p data-testid="p">{state}</p>
20+
}
21+
22+
const OuterComponent = () => {
23+
const [id, setId] = useState('1');
24+
25+
return <>
26+
<button data-testid="btn" onClick={() => setId('2')}>Change id</button>
27+
<SomeComponent id={id} />
28+
</>
29+
}
30+
31+
32+
describe('Deps change', () => {
33+
it('should subscribe to the new obseravble', () => {
34+
const { getByTestId } = render(<OuterComponent />);
35+
expect(getByTestId('p').innerHTML).toBe('1');
36+
fireEvent.click(getByTestId('btn'));
37+
expect(getByTestId('p').innerHTML).toBe('2');
38+
})
39+
})
40+
41+
describe('useObservable', () => {
42+
43+
beforeEach(() => jest.clearAllTimers());
2144

2245
it('should update every second', () => {
23-
const { result } = renderHook(() => useObservable(interval(1000), { initialValue: 0 }));
46+
const { result } = renderHook(() => useObservable(interval(1000), { initialValue: -1 }));
2447
let [next] = result.current;
2548

26-
expect(next).toBe(0);
49+
expect(next).toBe(-1);
2750

2851
act(() => {
2952
jest.advanceTimersByTime(1000);
@@ -42,27 +65,42 @@ describe('useObservable', () => {
4265
expect(next).toBe(1);
4366
});
4467

68+
69+
4570
it('should return an error', () => {
4671
const { result } = renderHook(() => useObservable(of(1).pipe(map(error => {
4772
throw new Error('error');
48-
})), { initialValue: 0 }));
73+
}))));
4974

5075
const [next, { error, completed }] = result.current;
5176

5277
expect(error).toEqual(new Error('error'));
53-
expect(next).toEqual(0);
78+
expect(next).toEqual(undefined);
5479
expect(completed).toEqual(false)
5580
})
5681

82+
83+
it('should support BehaviorSubject', () => {
84+
const query = new BehaviorSubject('init');
85+
const { result } = renderHook(() => useObservable(query));
86+
let [next] = result.current;
87+
88+
expect(next).toBe('init');
89+
90+
act(() => query.next('2'));
91+
92+
[next] = result.current;
93+
expect(next).toBe('2');
94+
});
5795
it('should unsubscribe', () => {
5896
const spy = jest.fn();
5997

60-
const { result, unmount } = renderHook(() => useObservable(interval(1000).pipe(finalize(spy)), { initialValue: 0 }));
98+
const { result, unmount } = renderHook(() => useObservable(interval(1000).pipe(finalize(spy)), { initialValue: -1 }));
6199

62-
// eslint-disable-next-line prefer-const
100+
// // eslint-disable-next-line prefer-const
63101
let [next] = result.current;
64102

65-
expect(next).toBe(0);
103+
expect(next).toBe(-1);
66104

67105
act(() => {
68106
jest.advanceTimersByTime(1000);
Lines changed: 30 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,52 @@
11
/* eslint-disable @typescript-eslint/no-non-null-assertion */
22
import { Observable, Subscription } from 'rxjs';
3-
import { useEffect, useRef, useState } from 'react';
3+
import { useEffect, useRef, useState, DependencyList, useMemo, useReducer } from 'react';
44

55
export function useObservable<T, E>(
66
source$: Observable<T>,
7-
{ initialValue }: { initialValue?: T | undefined } = {}
7+
{ deps = [], initialValue }: { deps?: DependencyList, initialValue?: T } = {}
88
): [T, { error: E | undefined, completed: boolean, subscription: Subscription | undefined }] {
9-
const sourceRef$ = useRef<Observable<T>>(source$);
10-
const subscription = useRef<Subscription | undefined>();
119

10+
const sourceRef = useMemo(() => source$, deps);
11+
const subscription = useRef(new Subscription());
12+
const nextValue = useRef<T | undefined>(initialValue);
13+
const [error, setError] = useState();
14+
const [completed, setCompleted] = useState<boolean>(false);
1215
const emitsInitialSyncValue = initialValue === undefined;
16+
const [_, forceUpdate] = useReducer(x => x + 1, 0);
1317

14-
const [error, setError] = useState<E | undefined>();
15-
const [completed, setComplete] = useState<boolean>(false);
16-
17-
const [next, setValue] = useState<T>(() => {
18+
useMemo(() => {
1819
if (emitsInitialSyncValue) {
19-
let firstValue: T | undefined = undefined;
20-
21-
let subscription: Subscription | null = sourceRef$.current.subscribe(
22-
(v) => {
23-
firstValue = v;
24-
}
25-
);
20+
let subscription: Subscription | null = sourceRef.subscribe(v => {
21+
nextValue.current = v;
22+
});
2623

2724
subscription.unsubscribe();
2825
subscription = null;
29-
30-
return firstValue! as T;
3126
}
32-
33-
return initialValue!;
34-
});
27+
}, deps);
3528

3629
useEffect(() => {
37-
const base = {
38-
error: setError,
39-
complete() {
40-
setComplete(true);
41-
},
42-
};
43-
if (emitsInitialSyncValue) {
44-
let firstEmission = true;
45-
subscription.current = sourceRef$.current.subscribe({
46-
next(v) {
47-
if (!firstEmission) {
48-
setValue(v);
49-
return;
50-
}
30+
let firstEmission = true;
31+
32+
subscription.current = sourceRef.subscribe({
33+
next(value) {
34+
if (emitsInitialSyncValue && firstEmission) {
5135
firstEmission = false;
52-
},
53-
...base,
54-
});
55-
} else {
56-
subscription.current = sourceRef$.current.subscribe({
57-
next: setValue,
58-
...base,
59-
});
60-
}
36+
} else {
37+
nextValue.current = value;
38+
forceUpdate();
39+
}
40+
},
41+
error: setError,
42+
complete: setCompleted.bind(null, true)
43+
})
6144

6245
return () => {
63-
if (!subscription.current?.closed) {
64-
subscription.current?.unsubscribe();
65-
}
66-
};
46+
subscription?.current.unsubscribe();
47+
}
48+
}, deps);
6749

68-
}, [emitsInitialSyncValue]);
6950

70-
return [next, { error, completed, subscription: subscription.current }];
51+
return [nextValue.current!, { error, completed, subscription: subscription.current }];
7152
}

0 commit comments

Comments
 (0)