Skip to content

Commit f5a84bf

Browse files
authored
Merge pull request #10 from LeetCode-OpenSource/refactor/state-observable
refactor: add state$ in hooks
2 parents 87681f1 + dbd91b5 commit f5a84bf

File tree

6 files changed

+230
-59
lines changed

6 files changed

+230
-59
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ declare function useObservable<T>(sourceFactory: () => Observable<T>): T | null
8181

8282
declare function useObservable<T>(sourceFactory: () => Observable<T>, initialState: T): T
8383

84-
declare function useObservable<T, U>(sourceFactory: (props$: Observable<U>) => Observable<T>, initialState: T, inputs: U): T
84+
declare function useObservable<T, U>(sourceFactory: (inputs$: Observable<U>) => Observable<T>, initialState: T, inputs: U): T
8585
```
8686

8787
#### Examples:
@@ -133,7 +133,7 @@ import { of } from 'rxjs'
133133
import { map } from 'rxjs/operators'
134134
135135
function App(props: { foo: number }) {
136-
const value = useObservable((props$) => props$.pipe(
136+
const value = useObservable((inputs$) => inputs$.pipe(
137137
map(([val]) => val + 1),
138138
), 200, [props.foo])
139139
return (

playground/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@ import { exhaustMap, mapTo, scan, switchMap } from 'rxjs/operators'
66
import { useObservable } from '../src/use-observable'
77
import { useEventCallback } from '../src/use-event-callback'
88

9-
const mockBackendRequest = (event$: Observable<React.SyntheticEvent<HTMLHeadElement>>) =>
9+
const mockBackendRequest = (event$: Observable<React.MouseEvent<HTMLHeadElement>>) =>
1010
event$.pipe(
1111
exhaustMap(() => timer(1000).pipe(mapTo(100))),
1212
scan((acc, cur) => acc + cur, 0),
1313
)
1414

1515
function IntervalValue(props: { interval: number }) {
16-
const [clickCallback, value] = useEventCallback<HTMLHeadingElement, number>(mockBackendRequest, 0)
16+
const [clickCallback, value] = useEventCallback(mockBackendRequest, 0, [])
1717
const intervalValue = useObservable(
18-
(props$) =>
19-
props$.pipe(
18+
(inputs$, _) =>
19+
inputs$.pipe(
2020
switchMap(([intervalTime]) => interval(intervalTime)),
2121
scan((acc) => acc + 1, 0),
2222
),

src/__test__/use-event-callback.spec.tsx

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react'
22
import { Observable, of, Observer } from 'rxjs'
3-
import { mapTo, delay } from 'rxjs/operators'
3+
import { mapTo, delay, withLatestFrom, map } from 'rxjs/operators'
44
import { create } from 'react-test-renderer'
55
import * as Sinon from 'sinon'
66

@@ -42,7 +42,7 @@ describe('useEventCallback specs', () => {
4242
expect(find(testRenderer.root, 'h1').children).toEqual([`${value}`])
4343
})
4444

45-
it('should trigger handler async callback', () => {
45+
it('should trigger handle async callback', () => {
4646
const timer = Sinon.useFakeTimers()
4747
const timeToDelay = 200
4848
const value = 1
@@ -63,7 +63,7 @@ describe('useEventCallback specs', () => {
6363
timer.restore()
6464
})
6565

66-
it('should handler the initial value', () => {
66+
it('should handle the initial value', () => {
6767
const timer = Sinon.useFakeTimers()
6868
const initialValue = 1000
6969
const value = 1
@@ -88,6 +88,89 @@ describe('useEventCallback specs', () => {
8888
timer.restore()
8989
})
9090

91+
it('should handle the state changed', () => {
92+
const timer = Sinon.useFakeTimers()
93+
const initialValue = 1000
94+
const value = 1
95+
const timeToDelay = 200
96+
const factory = (event$: Observable<React.MouseEvent<HTMLButtonElement>>, state$: Observable<number>) =>
97+
event$.pipe(
98+
withLatestFrom(state$),
99+
map(([_, state]) => {
100+
return state + value
101+
}),
102+
delay(timeToDelay),
103+
)
104+
function Fixture() {
105+
const [clickCallback, stateValue] = useEventCallback(factory, initialValue)
106+
107+
return (
108+
<>
109+
<h1>{stateValue}</h1>
110+
<button onClick={clickCallback}>click me</button>
111+
</>
112+
)
113+
}
114+
const fixtureNode = <Fixture />
115+
const testRenderer = create(fixtureNode)
116+
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue}`])
117+
testRenderer.update(fixtureNode)
118+
const button = find(testRenderer.root, 'button')
119+
button.props.onClick()
120+
timer.tick(timeToDelay)
121+
testRenderer.update(fixtureNode)
122+
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue + value}`])
123+
button.props.onClick()
124+
timer.tick(timeToDelay)
125+
testRenderer.update(fixtureNode)
126+
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue + value * 2}`])
127+
timer.restore()
128+
})
129+
130+
it('should handle the inputs changed', () => {
131+
const timer = Sinon.useFakeTimers()
132+
const initialValue = 1000
133+
const value = 1
134+
const timeToDelay = 200
135+
const factory = (
136+
event$: Observable<React.MouseEvent<HTMLButtonElement>>,
137+
inputs$: Observable<number[]>,
138+
_state$: Observable<number>,
139+
) =>
140+
event$.pipe(
141+
withLatestFrom(inputs$),
142+
map(([_, [count]]) => {
143+
return value + count
144+
}),
145+
delay(timeToDelay),
146+
)
147+
function Fixture(props: { count: number }) {
148+
const [clickCallback, stateValue] = useEventCallback(factory, initialValue, [props.count])
149+
150+
return (
151+
<>
152+
<h1>{stateValue}</h1>
153+
<button onClick={clickCallback}>click me</button>
154+
</>
155+
)
156+
}
157+
const fixtureNode = <Fixture count={1} />
158+
const testRenderer = create(fixtureNode)
159+
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue}`])
160+
testRenderer.update(fixtureNode)
161+
const button = find(testRenderer.root, 'button')
162+
button.props.onClick()
163+
timer.tick(timeToDelay)
164+
testRenderer.update(fixtureNode)
165+
expect(find(testRenderer.root, 'h1').children).toEqual([`${value + 1}`])
166+
testRenderer.update(<Fixture count={4} />)
167+
button.props.onClick()
168+
timer.tick(timeToDelay)
169+
testRenderer.update(<Fixture count={4} />)
170+
expect(find(testRenderer.root, 'h1').children).toEqual([`${value + 4}`])
171+
timer.restore()
172+
})
173+
91174
it('should call teardown logic after unmount', () => {
92175
const spy = Sinon.spy()
93176
const Fixture = createFixture(

src/__test__/use-observable.spec.tsx

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import React from 'react'
22
import { create } from 'react-test-renderer'
33
import * as Sinon from 'sinon'
4-
import { of, Observable, Observer } from 'rxjs'
4+
import { of, Observable, Observer, Subject } from 'rxjs'
55

66
import { find } from './find'
77
import { useObservable } from '../use-observable'
8-
import { tap } from 'rxjs/operators'
8+
import { tap, withLatestFrom, map } from 'rxjs/operators'
99

1010
describe('useObservable specs', () => {
11-
let timer: Sinon.SinonFakeTimers
11+
let fakeTimer: Sinon.SinonFakeTimers
1212

1313
beforeEach(() => {
14-
timer = Sinon.useFakeTimers()
14+
fakeTimer = Sinon.useFakeTimers()
1515
})
1616

1717
afterEach(() => {
18-
timer.restore()
18+
fakeTimer.restore()
1919
})
2020

2121
it('should get value from sync Observable', () => {
@@ -75,10 +75,51 @@ describe('useObservable specs', () => {
7575
expect(spy.callCount).toBe(1)
7676
})
7777

78+
it('should emit changed states in observableFactory', () => {
79+
const spy = Sinon.spy()
80+
const initialValue = 1000
81+
const source$ = new Subject<number>()
82+
function Fixture() {
83+
const value = useObservable((state$: Observable<number>) =>
84+
source$.pipe(
85+
withLatestFrom(state$),
86+
map(([intervalValue, state]) => {
87+
if (state) {
88+
return intervalValue + state
89+
}
90+
return intervalValue
91+
}),
92+
tap(spy),
93+
),
94+
)
95+
return (
96+
<>
97+
<h1>{value}</h1>
98+
</>
99+
)
100+
}
101+
102+
const testRenderer = create(<Fixture />)
103+
expect(spy.callCount).toBe(0)
104+
expect(find(testRenderer.root, 'h1').children).toEqual([])
105+
testRenderer.update(<Fixture />)
106+
source$.next(initialValue)
107+
expect(spy.callCount).toBe(1)
108+
expect(spy.args[0]).toEqual([initialValue])
109+
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue}`])
110+
111+
testRenderer.update(<Fixture />)
112+
const secondValue = 2000
113+
source$.next(secondValue)
114+
expect(spy.callCount).toBe(2)
115+
expect(spy.args[1]).toEqual([initialValue + secondValue])
116+
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue + secondValue}`])
117+
})
118+
78119
it('should emit changed props in observableFactory', () => {
79120
const spy = Sinon.spy()
80121
function Fixture(props: { foo: number; bar: string; baz: any }) {
81-
const value = useObservable((props$: Observable<[number, any]>) => props$.pipe(tap(spy)), null, [
122+
const value = useObservable((inputs$: Observable<[number, any]>) => inputs$.pipe(tap(spy)), null, [
82123
props.foo,
83124
props.baz,
84125
] as any)

src/use-event-callback.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,76 @@
1-
import { useEffect, useState, SyntheticEvent } from 'react'
2-
import { Observable, Subject, noop } from 'rxjs'
1+
import { useEffect, useMemo, useState, SyntheticEvent } from 'react'
2+
import { Observable, BehaviorSubject, Subject, noop } from 'rxjs'
33

4-
export type EventCallbackState<T, U> = [((e: SyntheticEvent<T>) => void) | typeof noop, U]
4+
export type VoidAsNull<T> = T extends void ? null : T
55

6-
export type EventCallback<T, U> = (eventSource$: Observable<SyntheticEvent<T>>) => Observable<U>
6+
export type EventCallbackState<_T, E, U, I = void> = [
7+
(e: E) => void,
8+
[U extends void ? null : U, BehaviorSubject<U | null>, BehaviorSubject<I | null>]
9+
]
10+
export type ReturnedState<T, E, U, I> = [EventCallbackState<T, E, U, I>[0], EventCallbackState<T, E, U, I>[1][0]]
711

8-
export function useEventCallback<T, U = void>(callback: EventCallback<T, U>): EventCallbackState<T, U | null>
9-
export function useEventCallback<T, U = void>(callback: EventCallback<T, U>, initialState: U): EventCallbackState<T, U>
12+
export type EventCallback<_T, E, U, I> = I extends void
13+
? (eventSource$: Observable<E>, state$: Observable<U>) => Observable<U>
14+
: (eventSource$: Observable<E>, inputs$: Observable<I>, state$: Observable<U>) => Observable<U>
1015

11-
export function useEventCallback<T, U = void>(
12-
callback: EventCallback<T, U>,
16+
export function useEventCallback<T, E extends SyntheticEvent<T>, U = void>(
17+
callback: EventCallback<T, E, U, void>,
18+
): ReturnedState<T, E, U | null, void>
19+
export function useEventCallback<T, E extends SyntheticEvent<T>, U = void>(
20+
callback: EventCallback<T, E, U, void>,
21+
initialState: U,
22+
): ReturnedState<T, E, U, void>
23+
export function useEventCallback<T, E extends SyntheticEvent<T>, U = void, I = void>(
24+
callback: EventCallback<T, E, U, I>,
25+
initialState: U,
26+
inputs: I,
27+
): ReturnedState<T, E, U, I>
28+
29+
export function useEventCallback<T, E extends SyntheticEvent<T>, U = void, I = void>(
30+
callback: EventCallback<T, E, U, I>,
1331
initialState?: U,
14-
): EventCallbackState<T, U | null> {
15-
const initialValue = typeof initialState !== 'undefined' ? initialState : null
16-
const [state, setState] = useState<EventCallbackState<T, U | null>>([noop, initialValue])
32+
inputs?: I,
33+
): ReturnedState<T, E, U | null, I> {
34+
const initialValue = (typeof initialState !== 'undefined' ? initialState : null) as VoidAsNull<U>
35+
const inputSubject$ = new BehaviorSubject<I | null>(typeof inputs === 'undefined' ? null : inputs)
36+
const stateSubject$ = new BehaviorSubject<U | null>(initialValue)
37+
const [state, setState] = useState(initialValue)
38+
const [returnedCallback, setEventCallback] = useState<(e: E) => void>(noop)
39+
const [state$] = useState(stateSubject$)
40+
const [inputs$] = useState(inputSubject$)
41+
42+
useMemo(() => {
43+
inputs$.next(inputs!)
44+
}, ((inputs as unknown) as ReadonlyArray<any>) || [])
45+
1746
useEffect(
1847
() => {
19-
const event$ = new Subject<SyntheticEvent<T>>()
20-
function eventCallback(e: SyntheticEvent<T>) {
48+
const event$ = new Subject<E>()
49+
function eventCallback(e: E) {
2150
return event$.next(e)
2251
}
23-
setState([eventCallback, initialValue])
24-
const value$ = callback(event$)
52+
setState(initialValue)
53+
setEventCallback(() => eventCallback)
54+
let value$: Observable<U>
55+
56+
if (!inputs) {
57+
value$ = (callback as EventCallback<T, E, U, void>)(event$, state$ as Observable<U>)
58+
} else {
59+
value$ = (callback as any)(event$, inputs$ as Observable<any>, state$ as Observable<U>)
60+
}
2561
const subscription = value$.subscribe((value) => {
26-
setState([eventCallback, value])
62+
state$.next(value)
63+
setState(value as VoidAsNull<U>)
2764
})
28-
return () => subscription.unsubscribe()
65+
return () => {
66+
subscription.unsubscribe()
67+
state$.complete()
68+
inputs$.complete()
69+
event$.complete()
70+
}
2971
},
30-
[0], // immutable forever
72+
[], // immutable forever
3173
)
3274

33-
return state
75+
return [returnedCallback, state]
3476
}

0 commit comments

Comments
 (0)