Skip to content
This repository has been archived by the owner on Jul 24, 2020. It is now read-only.

Commit

Permalink
feat: 新增 useEffectNovel; useNovel -> useMemoNovel
Browse files Browse the repository at this point in the history
  • Loading branch information
feichao93 committed Apr 2, 2020
1 parent 2d37e65 commit 5dc61cf
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 31 deletions.
16 changes: 10 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ https://zhuanlan.zhihu.com/p/92248348

## `Novel`

@little-saga/rx-hooks,Novel 是一种满足特定接口的函数。开发者将自定义的逻辑封装为相应的 novel,然后调用 useNovel 函数,使 novel 运行在一个 React 组件的生命周期内(基于 React hooks 机制)。
@little-saga/rx-hooks,Novel 是一种满足特定接口的函数。开发者将自定义的逻辑封装为相应的 novel,然后调用 useMemoNovel 函数,使 novel 运行在一个 React 组件的生命周期内(基于 React hooks 机制)。

Novel 内的逻辑一般采用 RxJS 编程,@little-saga/rx-hooks 不对 novel 内部的逻辑进行限制,开发者可以选用自己熟悉的 RxJS 开发方式。而 Novel 的输入/输出(即函数的参数和返回值)则需要满足下面描述的要求。

Expand All @@ -35,7 +35,7 @@ type Novel<I, S extends object, D extends object, E> = (
- novel 函数的参数用来抽象 `React -> RxJS` 的通信过程
- novel 函数被调用时,调用参数为 `input$``state$`
- input\$ 表示 React 当前向 novel 提供的输入,其类型为 `BehaviorSubject<I>`
- 每次组件的 render 方法被执行时,input\$ 中就会发出组件最新提供的值;值对应于 `useNovel(input, initState, novel)` 中的 input 参数
- 每次组件的 render 方法被执行时,input\$ 中就会发出组件最新提供的值;值对应于 `useMemoNovel(input, initState, novel)` 中的 input 参数
- 「input\$ 之于 novel」相当于「props 之于 React 组件」;input\$ 应当被认为是只读的
- state\$ 表示 novel 绑定到 React 组件的当前状态;其背后对应一个 useState hook
- 每次组件的 render 方法被执行时,novel 就可以通过 state\$ 获取到最新的状态
Expand All @@ -45,14 +45,14 @@ type Novel<I, S extends object, D extends object, E> = (
- novel 函数的返回值用来抽象 `RxJS -> React` 的通信过程
- novel 的返回值一般为一个对象,对象中各个字段的含义如下
- nextState:用于表示下一个状态的 Observable. 每当该 Observable 发出一个值的时候,对应的 useState hook 的 setState 方法将被调用,组件将重新渲染
- derived: 用于表示缓存/计算状态的 Observable. useNovel 的返回值中会包含该 Observable 的最新的值;该 Observable 发出值的时候不会触发渲染
- derived: 用于表示缓存/计算状态的 Observable. useMemoNovel 的返回值中会包含该 Observable 的最新的值;该 Observable 发出值的时候不会触发渲染
- exports: 对象导出,使得 React 组件可以获取到 novel 内部的对象
- teardown: 清理逻辑,当组件被卸载时,该函数将被调用
- novel 的返回值也可以是一个简单的 Observable,此时该 Observable 的作用与 nextState 字段相同。
## API
### `useNovel(input: I, initState: S, novel: Novel<I, S, D, E>) => [S & D, E]`
### `useMemoNovel(input: I, initState: S, novel: Novel<I, S, D, E>) => [S & D, E]`
泛型参数说明:
Expand All @@ -61,6 +61,10 @@ type Novel<I, S extends object, D extends object, E> = (
- `D` novel 返回值中 derived 字段的类型
- `E` novel 返回值中 exports 字段的类型
useNovel 将在一个 React 组件内执行 novel 函数,并将 novel 的输入输出与 React 组件绑定起来。注意 useNovel 是一个 React hooks,调用该函数需要遵循 [hooks rules](https://zh-hans.reactjs.org/docs/hooks-rules.html).
useMemoNovel 将在一个 React 组件内执行 novel 函数,并将 novel 的输入输出与 React 组件绑定起来。注意 useMemoNovel 是一个 React hooks,调用该函数需要遵循 [hooks rules](https://zh-hans.reactjs.org/docs/hooks-rules.html).
useNovel 会返回一个数组,数组的长度固定为 2,第一个元素是 state 与 derived 两个对象的合并结果,第二个元素即为 novel 返回对象的 exports 字段。
useMemoNovel 会返回一个数组,数组的长度固定为 2,第一个元素是 state 与 derived 两个对象的合并结果,第二个元素即为 novel 返回对象的 exports 字段。
### `useMemoNovel`
`useMemoNovel` 相似,但是 novel 函数会在 useEffect 会被调用
12 changes: 6 additions & 6 deletions src/__tests__/novel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { combineLatest, interval, isObservable, NEVER, Observable, Subscription
import { map, startWith, switchMap } from 'rxjs/operators'
import { SubjectProxy } from '../helpers'
import { StateObservable } from '../index'
import { useNovel } from '../novel'
import { useMemoNovel } from '../memo-novel'
import { applyMutatorAsReducer } from '../operators'

describe('simpleCounterNovel', () => {
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('simpleCounterNovel', () => {
test('simple inc/dec/reset case', () => {
const { result } = renderHook(
({ initCount }: { initCount: number }) =>
useNovel({ initCount }, { count: 0 }, simpleCounterNovel),
useMemoNovel({ initCount }, { count: 0 }, simpleCounterNovel),
{ initialProps: { initCount: 0 } },
)

Expand Down Expand Up @@ -76,7 +76,7 @@ describe('simpleCounterNovel', () => {
test('derived and exports', () => {
const { result } = renderHook(
({ initCount }: { initCount: number }) =>
useNovel({ initCount }, { count: 0 }, simpleCounterNovel),
useMemoNovel({ initCount }, { count: 0 }, simpleCounterNovel),
{ initialProps: { initCount: 0 } },
)

Expand Down Expand Up @@ -106,7 +106,7 @@ test('derived$ not emitting a value synchronously should throw', () => {
}
}

const { result } = renderHook(() => useNovel(null, null, flawNovel))
const { result } = renderHook(() => useMemoNovel(null, null, flawNovel))

expect(result.error.message).toMatch('derived$ must synchronously emit a value.')
})
Expand All @@ -128,7 +128,7 @@ test('novel directly return nextState$ and novel should unsubscribe when unmount
}

const { result, unmount, waitForNextUpdate } = renderHook(() =>
useNovel(null, { count: 0 }, novel),
useMemoNovel(null, { count: 0 }, novel),
)

expect(result.current[0].count).toBe(0)
Expand All @@ -152,7 +152,7 @@ test('novel should call teardown() when unmount', () => {
}
}

const { unmount } = renderHook(() => useNovel(null, null, novel))
const { unmount } = renderHook(() => useMemoNovel(null, null, novel))

expect(teardown).not.toBeCalled()
unmount()
Expand Down
74 changes: 74 additions & 0 deletions src/effect-novel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { isObservable, Subject, Subscription } from 'rxjs'
import { Novel } from './interfaces'
import StateObservable from './StateObservable'

export function useEffectNovel<I, S extends object, D extends object, E>(
input: I,
initialState: S,
novel: Novel<I, S, D, E>,
) {
const [state, setState] = useState(initialState)
const input$ = useMemo(() => new Subject<I>(), [])
const state$ = useMemo(() => new Subject<S>(), [])

// 每次渲染之后更新 input$
const mount = useRef(true)
useEffect(() => {
// 跳过第一次渲染
if (mount.current) {
mount.current = false
return
}
input$.next(input)
})

const derivedValueRef = useRef<D>(null)
const exportsRef = useRef<E>(null)
const subscription = useMemo(() => new Subscription(), [])

// 用 useEffect 来执行 novel 与订阅 derived$,注意这一个操作是「异步」的
// derived/nextState 的初始值会被丢弃
useEffect(() => {
const output = novel(
new StateObservable(input$, input),
new StateObservable(state$, initialState),
)
if (output == null) {
return
}

if (isObservable(output)) {
subscription.add(
output.subscribe(value => {
state$.next(value)
setState(value)
}),
)
} else {
subscription.add(
output.nextState?.subscribe(value => {
state$.next(value)
setState(value)
}),
)
if (output.derived) {
subscription.add(
output.derived.subscribe(value => {
derivedValueRef.current = value
}),
)
}
subscription.add(output.teardown)
exportsRef.current = output.exports
}
}, [])

useEffect(() => {
return () => {
subscription.unsubscribe()
}
}, [])

return [{ ...state, ...derivedValueRef.current }, exportsRef.current] as const
}
16 changes: 12 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export * from './equal-fns'
export * from './helpers'
export * from './novel'
export * from './operators'
export { deepEqual, shallowEqual } from './equal-fns'
export { SubjectProxy } from './helpers'
export { combineLatestFromObject, applyMutatorAsReducer, log, ofType } from './operators'
export { default as StateObservable } from './StateObservable'
export { Novel} from './interfaces'
export { useMemoNovel } from './memo-novel'
export { useEffectNovel } from './effect-novel'
export { NO_VALUE } from './memo-novel'

import { useMemoNovel } from './memo-novel'

/** @deprecated Use {useMemoNovel} instead */
export const useNovel = useMemoNovel
14 changes: 14 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Observable } from 'rxjs'
import StateObservable from './StateObservable'

export type Novel<I, S extends object, D extends object, E> = (
input$: StateObservable<I>,
state$: StateObservable<S>,
) =>
| Observable<S>
| {
nextState?: Observable<S>
derived?: Observable<D>
exports?: E
teardown?(): void
}
19 changes: 4 additions & 15 deletions src/novel.ts → src/memo-novel.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { isObservable, Observable, Subject, Subscription } from 'rxjs'
import { isObservable, Subject, Subscription } from 'rxjs'
import { Novel } from './interfaces'
import StateObservable from './StateObservable'

const NO_VALUE = Symbol('no-value')
export const NO_VALUE = Symbol('no-value')

export type Novel<I, S extends object, D extends object, E> = (
input$: StateObservable<I>,
state$: StateObservable<S>,
) =>
| Observable<S>
| {
nextState?: Observable<S>
derived?: Observable<D>
exports?: E
teardown?(): void
}

export function useNovel<I, S extends object, D extends object, E>(
export function useMemoNovel<I, S extends object, D extends object, E>(
input: I,
initialState: S,
novel: Novel<I, S, D, E>,
Expand Down

0 comments on commit 5dc61cf

Please sign in to comment.