Easily integrate rxjs into react hooks! observable-hooks source code analysis!

foreword

A former colleague of mine just joined a local state-owned enterprise in Chengdu (it’s too enviable to get off work at 5 o’clock). They used the observable-hooks library, which I haven’t used before. I only know a library called rxjs-hooks from the leetcode front-end team. , I tried to go to the official website to see it, and found that observable-hooks are indeed better in ability

But to be honest, the documents written on the official website are really difficult to understand for people with average rxjs proficiency. So this article is made to learn with everyone. Each API has an online case, which is convenient for everyone to play by themselves.

Core API source code explanation

useObservable

This API is generally used to monitor value changes and then return an Observable, which has the advantage of being responsive like vue and mobx.

In a nutshell: listen for value changes and then generate a new stream, use this method, the case is as follows:

Click the button, online address:

codesandbox.io/s/sharp-haw…

import "./styles.css";
import { useObservable } from "observable-hooks";
import { map } from "rxjs";
import { useState } from "react";

const App = (props) => {
  const [showPanel] = useState("hello");

  // 监听 props 或 state 变化
  const enhanced$ = useObservable(
    (inputs$) => inputs$.pipe(map(([showPanel]) => showPanel + "world")),
    [showPanel]
  );
  return (
    <div className="App">
      <h1>{showPanel}</h1>
      <button
        onClick={() => {
          // 这个方法
          enhanced$.subscribe((value) => alert(value));
        }}
      >
        click
      </button>
    </div>
  );
};

export default App;
复制代码

Source code analysis, key code:

export function useObservable(
  init,
  inputs?: [...TInputs]
): Observable<TOutput> {

  const inputs$Ref = useRefFn(() => new BehaviorSubject(inputs))
  const source$Ref = useRefFn(() => init(inputs$Ref.current))

  const firstEffectRef = useRef(true)
  useEffect(() => {
    if (firstEffectRef.current) {
      firstEffectRef.current = false
      return
    }
    inputs$Ref.current.next(inputs)
  }, inputs)

  return source$Ref.current
}
复制代码

useRefFn uses useRef to make new BehaviorSubject(inputs), instead of instantiating new every time, we can reuse the new BehaviorSubject generated for the first time. The source code is very simple, please note that init() will only be called once, as follows:

/**
 * 一个返回值的函数。 只会被调用一次
 */
 export function useRefFn<T>(init: () => T) {
  const firstRef = useRef(true)
  // 请注意init() 只会调用一次
  const ref = useRef<T | null>(null)
  if (firstRef.current) {
    firstRef.current = false
    ref.current = init()
  }
  return ref;
}
复制代码

Well, let's look at the useObservable source code, first create a new BehaviorSubject(inputs), inputs is the second parameter of useObservable, depending on the data, useObservable will push the stream again when these data changes.

The code to re-push is:

  useEffect(() => {
    if (firstEffectRef.current) {
      firstEffectRef.current = false
      return
    }
    inputs$Ref.current.next(inputs)
  }, inputs)
复制代码

It can be seen that useEffect is used to monitor the changes of inputs, and then inputs$Ref.current.next(inputs) to push the stream again.

Finally, let's look at this sentence

  const source$Ref = useRefFn(() => init(inputs$Ref.current))
复制代码

It is to pass the new BehaviorSubject(inputs) stream to the init function. The init function is the first parameter of useObservable, which is customized by us.

useLayoutObservable

与 useObservable 基本一样,不同的是底下使用 useLayoutEffect 监听改变。

如果需要在下次浏览器绘制前拿到值可以用它, 所以源码跟我们之前是一样的,就是把useEffect改成了useLayoutEffect而已。

useObservableCallback

一言以蔽之,这个useObservableCallback一般用来给事件监听的,事件一变化就产生新的流。需要注意的是,需要自己手动去订阅。

案例如下(当input值变化时,注意看控制台信息变化): codesandbox.io/s/affection…

import "./styles.css";
import { pluck, map } from "rxjs";
import { useObservableCallback } from "observable-hooks";
import { useEffect } from "react";

const App = (props) => {
  const [onChange, outputs$] = useObservableCallback((event$) =>
    event$.pipe(pluck("currentTarget", "value"))
  );
  useEffect(() => outputs$.subscribe((v) => console.log(v)));
  return <input type="text" onChange={onChange} />;
};

export default App;
复制代码

源码如下:

export function useObservableCallback(
  init,
  selector
) {
  const events$Ref = useRefFn(new Subject())
  const outputs$Ref = useRefFn(() => init(events$Ref.current))
  const callbackRef = useRef((...args) => {
    events$Ref.current.next(selector ? selector(args) : args[0])
  })
  return [callbackRef.current, outputs$Ref.current]
}
复制代码
  • 首先events$Ref就是一个new Subject
  • 然后定义一个消费流outputs$Ref,我们传入的自定义init函数第一个参数就是上一步的new Subject
  • callbackRef是一个注册函数,常用于给事件,也就是事件触发,就给outputs$Ref推送数据

当然需要回调函数传入多个参数才需要selector,大家现在只是入门,等用到的时候再了解不迟。

useSubscription

useSubscription说白了,就是subscribe的hooks而已。

源码如下:

源码只做了一个特殊处理需要注意,其他的不用看,就是subscrible

if (input$ !== argsRef.current[0]) {
     // stale observable
     return
}
复制代码
export function useSubscriptionInternal(
  args
) {
  const argsRef = useRef(args)
  const subscriptionRef = useRef()

  useEffect(() => {
    argsRef.current = args
  })

  useEffect(() => {
    const input$ = argsRef.current[0]

    const subscription = input$.subscribe({
      next: value => {
        if (input$ !== argsRef.current[0]) {
          // stale observable
          return
        }
        const nextObserver =
          argsRef.current[1].next ||
          argsRef.current[1]
        if (nextObserver) {
          return nextObserver(value)
        }
      },
      error: error => {
        if (input$ !== argsRef.current[0]) {
          // stale observable
          return
        }
        const errorObserver =
          argsRef.current[1].error ||
          argsRef.current[2]
        if (errorObserver) {
          return errorObserver(error)
        }
        console.error(error)
      },
      complete: () => {
        if (input$ !== argsRef.current[0]) {
          // stale observable
          return
        }
        const completeObserver =
          argsRef.current[1].complete ||
          argsRef.current[3]
        if (completeObserver) {
          return completeObserver()
        }
      }
    })

    subscriptionRef.current = subscription

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

  return subscriptionRef
}
复制代码

useLayoutSubscription

与 useSubscription 一样,除了 subscription 是通过 useLayoutEffect 触发。

当需要在 DOM 绘制前拿到值时会有用。

尽量少用,因为其是在浏览器绘制前同步调用。过多的同步值产生会延长组件的 commit 周期。

useObservableState

This function can be used as useState or useReducer. I suggest that you use useReducer instead of state for your own projects, because generally your state is classified. For example, when you request a piece of data, this data is a variable, but it is also accompanied by the fact that the request The loading state of the data, the loading and the requested data are integrated. Why use two useStates? It looks really awkward.

The case plus one minus one is as follows:

Online code: codesandbox.io/s/kind-jasp…

import "./styles.css";
import { scan } from "rxjs";
import { useObservableState } from "observable-hooks";

const App = (props) => {
  const [state, dispatch] = useObservableState(
    (action$, initialState) =>
      action$.pipe(
        scan((state, action) => {
          switch (action.type) {
            case "INCREMENT":
              return {
                ...state,
                count:
                  state.count + (isNaN(action.payload) ? 1 : action.payload)
              };
            case "DECREMENT":
              return {
                ...state,
                count:
                  state.count - (isNaN(action.payload) ? 1 : action.payload)
              };
            default:
              return state;
          }
        }, initialState)
      ),
    () => ({ count: 0 })
  );

  return (
    <div className="App">
      <h1>{state.count}</h1>
      <button
        onClick={() => {
          dispatch({ type: "INCREMENT" });
        }}
      >
        加一
      </button>
      <button
        onClick={() => {
          dispatch({ type: "DECREMENT" });
        }}
      >
        减一
      </button>
    </div>
  );
};

export default App;
复制代码

So we only introduce how to use useObservableState to implement useReducer here, the following is the key code

  • state is the data we want
  • The callback will pass the value you want to pass into the first parameter state$OrInit, which is our custom stream
  • useSubscription will eventually return the latest state of the processed data setState in the stream
useObservableStateInternal(
  state$OrInit,
  initialState){
    const init = state$OrInit
    const [state, setState] = useState(initialState)

    const input$Ref = useRefFn(new Subject())

    const state$ = useRefFn(() => init(input$Ref.current, state)).current
    const callback = useRef((state) =>
      input$Ref.current.next(state)
    ).current

    useSubscription(state$, setState)

    return [state, callback]

}
复制代码

end

Guess you like

Origin juejin.im/post/7079704126548312100