Skip to content

Commit c89b98e

Browse files
committed
refactor: add state$ in hooks
1 parent 2f48f43 commit c89b98e

File tree

5 files changed

+238
-54
lines changed

5 files changed

+238
-54
lines changed

playground/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ 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$) =>
18+
(props$, _) =>
1919
props$.pipe(
2020
switchMap(([intervalTime]) => interval(intervalTime)),
2121
scan((acc) => acc + 1, 0),

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

Lines changed: 84 additions & 1 deletion
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

@@ -88,6 +88,89 @@ describe('useEventCallback specs', () => {
8888
timer.restore()
8989
})
9090

91+
it('should handler 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 handler 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+
_state$: Observable<number>,
138+
props$: Observable<[number]>,
139+
) =>
140+
event$.pipe(
141+
withLatestFrom(props$),
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] as [number])
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: 46 additions & 5 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,6 +75,47 @@ 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 }) {

src/use-event-callback.ts

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,84 @@
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>, state$: Observable<U>, inputs$: Observable<I>) => 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 [[returnedCallback, [state, state$, inputs$]], setState] = useState<EventCallbackState<T, E, U, I>>([
38+
noop,
39+
[initialValue, stateSubject$, inputSubject$],
40+
])
41+
42+
if (inputs) {
43+
useMemo(
44+
() => {
45+
inputs$.next(inputs)
46+
},
47+
inputs as any,
48+
)
49+
}
50+
1751
useEffect(
1852
() => {
19-
const event$ = new Subject<SyntheticEvent<T>>()
20-
function eventCallback(e: SyntheticEvent<T>) {
53+
const event$ = new Subject<E>()
54+
function eventCallback(e: E) {
2155
return event$.next(e)
2256
}
23-
setState([eventCallback, initialValue])
24-
const value$ = callback(event$)
57+
setState([eventCallback, [initialValue, state$, inputs$]])
58+
let value$
59+
60+
if (!inputs) {
61+
value$ = (callback as EventCallback<T, E, U, void>)(event$, state$ as Observable<U>)
62+
} else {
63+
value$ = (callback as EventCallback<T, E, U, ReadonlyArray<any>>)(
64+
event$,
65+
state$ as Observable<U>,
66+
inputs$ as Observable<any>,
67+
)
68+
}
2569
const subscription = value$.subscribe((value) => {
26-
setState([eventCallback, value])
70+
state$.next(value)
71+
setState([eventCallback, [value as VoidAsNull<U>, state$, inputs$]])
2772
})
28-
return () => subscription.unsubscribe()
73+
return () => {
74+
subscription.unsubscribe()
75+
state$.complete()
76+
inputs$.complete()
77+
event$.complete()
78+
}
2979
},
30-
[0], // immutable forever
80+
[], // immutable forever
3181
)
3282

33-
return state
83+
return [returnedCallback, state]
3484
}

src/use-observable.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,52 @@ import { Observable, BehaviorSubject } from 'rxjs'
22
import { useState, useEffect, useMemo } from 'react'
33

44
export type InputFactory<T, U = undefined> = U extends undefined
5-
? () => Observable<T>
6-
: (props$: Observable<U>) => Observable<T>
5+
? (state$: Observable<T>) => Observable<T>
6+
: (props$: Observable<U>, state$: Observable<T>) => Observable<T>
77

88
export function useObservable<T>(inputFactory: InputFactory<T>): T | null
99
export function useObservable<T>(inputFactory: InputFactory<T>, initialState: T): T
10-
export function useObservable<T, U extends ReadonlyArray<any>>(
11-
inputFactory: InputFactory<T, U>,
12-
initialState: T,
13-
inputs: U,
14-
): T
10+
export function useObservable<T, U>(inputFactory: InputFactory<T, U>, initialState: T, inputs: U): T
1511

16-
export function useObservable<T, U extends ReadonlyArray<any> | undefined>(
17-
inputFactory: InputFactory<T, U>,
18-
initialState?: T,
19-
inputs?: U,
20-
): T | null {
21-
const [inputs$] = useState(new BehaviorSubject<U | undefined>(inputs))
22-
const [state, setState] = useState(typeof initialState !== 'undefined' ? initialState : null)
12+
export function useObservable<T, U>(inputFactory: InputFactory<T, U>, initialState?: T, inputs?: U): T | null {
13+
const stateSubject$ = new BehaviorSubject<T | undefined>(initialState)
14+
const inputSubject$ = new BehaviorSubject<U | undefined>(inputs)
15+
const [[state, state$, inputs$], setState] = useState<
16+
[T | null, BehaviorSubject<T | undefined>, BehaviorSubject<U | undefined>]
17+
>([typeof initialState !== 'undefined' ? initialState : null, stateSubject$, inputSubject$])
18+
19+
if (inputs) {
20+
useMemo(
21+
() => {
22+
inputs$.next(inputs)
23+
},
24+
(inputs as unknown) as ReadonlyArray<any>,
25+
)
26+
}
2327

2428
useEffect(
2529
() => {
26-
const output$ = (inputFactory as (inputs$?: Observable<U | undefined>) => Observable<T>)(
27-
typeof inputs !== 'undefined' ? inputs$ : void 0,
28-
)
29-
const subscription = output$.subscribe((value) => setState(value))
30-
return () => subscription.unsubscribe()
30+
let output$: BehaviorSubject<T>
31+
if (inputs) {
32+
output$ = (inputFactory as (
33+
inputs$: Observable<U | undefined>,
34+
state$: Observable<T | undefined>,
35+
) => Observable<T>)(inputs$, state$) as BehaviorSubject<T>
36+
} else {
37+
output$ = (inputFactory as (state$: Observable<T | undefined>) => Observable<T>)(state$) as BehaviorSubject<T>
38+
}
39+
const subscription = output$.subscribe((value) => {
40+
state$.next(value)
41+
setState([value, state$, inputs$])
42+
})
43+
return () => {
44+
subscription.unsubscribe()
45+
inputs$.complete()
46+
state$.complete()
47+
}
3148
},
3249
[], // immutable forever
3350
)
3451

35-
useMemo(
36-
() => {
37-
inputs$.next(inputs)
38-
},
39-
(inputs || []) as ReadonlyArray<any>,
40-
)
41-
4252
return state
4353
}

0 commit comments

Comments
 (0)