Skip to content

refactor: add state$ in hooks #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ declare function useObservable<T>(sourceFactory: () => Observable<T>): T | null

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

declare function useObservable<T, U>(sourceFactory: (props$: Observable<U>) => Observable<T>, initialState: T, inputs: U): T
declare function useObservable<T, U>(sourceFactory: (inputs$: Observable<U>) => Observable<T>, initialState: T, inputs: U): T
```

#### Examples:
Expand Down Expand Up @@ -133,7 +133,7 @@ import { of } from 'rxjs'
import { map } from 'rxjs/operators'

function App(props: { foo: number }) {
const value = useObservable((props$) => props$.pipe(
const value = useObservable((inputs$) => inputs$.pipe(
map(([val]) => val + 1),
), 200, [props.foo])
return (
Expand Down
8 changes: 4 additions & 4 deletions playground/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import { exhaustMap, mapTo, scan, switchMap } from 'rxjs/operators'
import { useObservable } from '../src/use-observable'
import { useEventCallback } from '../src/use-event-callback'

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

function IntervalValue(props: { interval: number }) {
const [clickCallback, value] = useEventCallback<HTMLHeadingElement, number>(mockBackendRequest, 0)
const [clickCallback, value] = useEventCallback(mockBackendRequest, 0, [])
const intervalValue = useObservable(
(props$) =>
props$.pipe(
(inputs$, _) =>
inputs$.pipe(
switchMap(([intervalTime]) => interval(intervalTime)),
scan((acc) => acc + 1, 0),
),
Expand Down
89 changes: 86 additions & 3 deletions src/__test__/use-event-callback.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react'
import { Observable, of, Observer } from 'rxjs'
import { mapTo, delay } from 'rxjs/operators'
import { mapTo, delay, withLatestFrom, map } from 'rxjs/operators'
import { create } from 'react-test-renderer'
import * as Sinon from 'sinon'

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

it('should trigger handler async callback', () => {
it('should trigger handle async callback', () => {
const timer = Sinon.useFakeTimers()
const timeToDelay = 200
const value = 1
Expand All @@ -63,7 +63,7 @@ describe('useEventCallback specs', () => {
timer.restore()
})

it('should handler the initial value', () => {
it('should handle the initial value', () => {
const timer = Sinon.useFakeTimers()
const initialValue = 1000
const value = 1
Expand All @@ -88,6 +88,89 @@ describe('useEventCallback specs', () => {
timer.restore()
})

it('should handle the state changed', () => {
const timer = Sinon.useFakeTimers()
const initialValue = 1000
const value = 1
const timeToDelay = 200
const factory = (event$: Observable<React.MouseEvent<HTMLButtonElement>>, state$: Observable<number>) =>
event$.pipe(
withLatestFrom(state$),
map(([_, state]) => {
return state + value
}),
delay(timeToDelay),
)
function Fixture() {
const [clickCallback, stateValue] = useEventCallback(factory, initialValue)

return (
<>
<h1>{stateValue}</h1>
<button onClick={clickCallback}>click me</button>
</>
)
}
const fixtureNode = <Fixture />
const testRenderer = create(fixtureNode)
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue}`])
testRenderer.update(fixtureNode)
const button = find(testRenderer.root, 'button')
button.props.onClick()
timer.tick(timeToDelay)
testRenderer.update(fixtureNode)
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue + value}`])
button.props.onClick()
timer.tick(timeToDelay)
testRenderer.update(fixtureNode)
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue + value * 2}`])
timer.restore()
})

it('should handle the inputs changed', () => {
const timer = Sinon.useFakeTimers()
const initialValue = 1000
const value = 1
const timeToDelay = 200
const factory = (
event$: Observable<React.MouseEvent<HTMLButtonElement>>,
inputs$: Observable<number[]>,
_state$: Observable<number>,
) =>
event$.pipe(
withLatestFrom(inputs$),
map(([_, [count]]) => {
return value + count
}),
delay(timeToDelay),
)
function Fixture(props: { count: number }) {
const [clickCallback, stateValue] = useEventCallback(factory, initialValue, [props.count])

return (
<>
<h1>{stateValue}</h1>
<button onClick={clickCallback}>click me</button>
</>
)
}
const fixtureNode = <Fixture count={1} />
const testRenderer = create(fixtureNode)
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue}`])
testRenderer.update(fixtureNode)
const button = find(testRenderer.root, 'button')
button.props.onClick()
timer.tick(timeToDelay)
testRenderer.update(fixtureNode)
expect(find(testRenderer.root, 'h1').children).toEqual([`${value + 1}`])
testRenderer.update(<Fixture count={4} />)
button.props.onClick()
timer.tick(timeToDelay)
testRenderer.update(<Fixture count={4} />)
expect(find(testRenderer.root, 'h1').children).toEqual([`${value + 4}`])
timer.restore()
})

it('should call teardown logic after unmount', () => {
const spy = Sinon.spy()
const Fixture = createFixture(
Expand Down
53 changes: 47 additions & 6 deletions src/__test__/use-observable.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import React from 'react'
import { create } from 'react-test-renderer'
import * as Sinon from 'sinon'
import { of, Observable, Observer } from 'rxjs'
import { of, Observable, Observer, Subject } from 'rxjs'

import { find } from './find'
import { useObservable } from '../use-observable'
import { tap } from 'rxjs/operators'
import { tap, withLatestFrom, map } from 'rxjs/operators'

describe('useObservable specs', () => {
let timer: Sinon.SinonFakeTimers
let fakeTimer: Sinon.SinonFakeTimers

beforeEach(() => {
timer = Sinon.useFakeTimers()
fakeTimer = Sinon.useFakeTimers()
})

afterEach(() => {
timer.restore()
fakeTimer.restore()
})

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

it('should emit changed states in observableFactory', () => {
const spy = Sinon.spy()
const initialValue = 1000
const source$ = new Subject<number>()
function Fixture() {
const value = useObservable((state$: Observable<number>) =>
source$.pipe(
withLatestFrom(state$),
map(([intervalValue, state]) => {
if (state) {
return intervalValue + state
}
return intervalValue
}),
tap(spy),
),
)
return (
<>
<h1>{value}</h1>
</>
)
}

const testRenderer = create(<Fixture />)
expect(spy.callCount).toBe(0)
expect(find(testRenderer.root, 'h1').children).toEqual([])
testRenderer.update(<Fixture />)
source$.next(initialValue)
expect(spy.callCount).toBe(1)
expect(spy.args[0]).toEqual([initialValue])
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue}`])

testRenderer.update(<Fixture />)
const secondValue = 2000
source$.next(secondValue)
expect(spy.callCount).toBe(2)
expect(spy.args[1]).toEqual([initialValue + secondValue])
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue + secondValue}`])
})

it('should emit changed props in observableFactory', () => {
const spy = Sinon.spy()
function Fixture(props: { foo: number; bar: string; baz: any }) {
const value = useObservable((props$: Observable<[number, any]>) => props$.pipe(tap(spy)), null, [
const value = useObservable((inputs$: Observable<[number, any]>) => inputs$.pipe(tap(spy)), null, [
props.foo,
props.baz,
] as any)
Expand Down
80 changes: 61 additions & 19 deletions src/use-event-callback.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,76 @@
import { useEffect, useState, SyntheticEvent } from 'react'
import { Observable, Subject, noop } from 'rxjs'
import { useEffect, useMemo, useState, SyntheticEvent } from 'react'
import { Observable, BehaviorSubject, Subject, noop } from 'rxjs'

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

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

export function useEventCallback<T, U = void>(callback: EventCallback<T, U>): EventCallbackState<T, U | null>
export function useEventCallback<T, U = void>(callback: EventCallback<T, U>, initialState: U): EventCallbackState<T, U>
export type EventCallback<_T, E, U, I> = I extends void
? (eventSource$: Observable<E>, state$: Observable<U>) => Observable<U>
: (eventSource$: Observable<E>, inputs$: Observable<I>, state$: Observable<U>) => Observable<U>

export function useEventCallback<T, U = void>(
callback: EventCallback<T, U>,
export function useEventCallback<T, E extends SyntheticEvent<T>, U = void>(
callback: EventCallback<T, E, U, void>,
): ReturnedState<T, E, U | null, void>
export function useEventCallback<T, E extends SyntheticEvent<T>, U = void>(
callback: EventCallback<T, E, U, void>,
initialState: U,
): ReturnedState<T, E, U, void>
export function useEventCallback<T, E extends SyntheticEvent<T>, U = void, I = void>(
callback: EventCallback<T, E, U, I>,
initialState: U,
inputs: I,
): ReturnedState<T, E, U, I>

export function useEventCallback<T, E extends SyntheticEvent<T>, U = void, I = void>(
callback: EventCallback<T, E, U, I>,
initialState?: U,
): EventCallbackState<T, U | null> {
const initialValue = typeof initialState !== 'undefined' ? initialState : null
const [state, setState] = useState<EventCallbackState<T, U | null>>([noop, initialValue])
inputs?: I,
): ReturnedState<T, E, U | null, I> {
const initialValue = (typeof initialState !== 'undefined' ? initialState : null) as VoidAsNull<U>
const inputSubject$ = new BehaviorSubject<I | null>(typeof inputs === 'undefined' ? null : inputs)
const stateSubject$ = new BehaviorSubject<U | null>(initialValue)
const [state, setState] = useState(initialValue)
const [returnedCallback, setEventCallback] = useState<(e: E) => void>(noop)
const [state$] = useState(stateSubject$)
const [inputs$] = useState(inputSubject$)

useMemo(() => {
inputs$.next(inputs!)
}, ((inputs as unknown) as ReadonlyArray<any>) || [])

useEffect(
() => {
const event$ = new Subject<SyntheticEvent<T>>()
function eventCallback(e: SyntheticEvent<T>) {
const event$ = new Subject<E>()
function eventCallback(e: E) {
return event$.next(e)
}
setState([eventCallback, initialValue])
const value$ = callback(event$)
setState(initialValue)
setEventCallback(() => eventCallback)
let value$: Observable<U>

if (!inputs) {
value$ = (callback as EventCallback<T, E, U, void>)(event$, state$ as Observable<U>)
} else {
value$ = (callback as any)(event$, inputs$ as Observable<any>, state$ as Observable<U>)
}
const subscription = value$.subscribe((value) => {
setState([eventCallback, value])
state$.next(value)
setState(value as VoidAsNull<U>)
})
return () => subscription.unsubscribe()
return () => {
subscription.unsubscribe()
state$.complete()
inputs$.complete()
event$.complete()
}
},
[0], // immutable forever
[], // immutable forever
)

return state
return [returnedCallback, state]
}
Loading