crimx / observable-hooks

⚛️☯️💪 React hooks for RxJS Observables. Concurrent mode safe.
https://observable-hooks.js.org
MIT License
1.03k stars 44 forks source link

如何实现替换react官网的useEventCallback? #95

Closed dragooncjw closed 2 years ago

dragooncjw commented 2 years ago

下图是react官网对useEventCallback的描述,文档里表示在任何情况下都不建议使用这个函数, image 我想使用observable-hooks库尝试实现替换react原生不被推荐的useEventCallback

但是看了源码,发现useSubscription内的observer也是使用useRef实现的,那这样还是会存在渲染阶段的问题,我数据获取的时候只会获取到useRef初始化地方的变量,如果渲染阶段,在useRef后变量更新了,或者是在React并发模式下,还是会存在和useEventCallback一样的问题。

我如果将所有依赖都切换成Observable感觉处理起来会很麻烦,请问官方是否有合理的替代方案呢?

下面是我的代码:

export default function usePersistCallback<T>(
  observer?: (value: T) => void,
  init?: (events$: Observable<T>) => Observable<T>,
) {
  const [onEventTrigger, onEventTrigger$] = useObservableCallback(init);

  useSubscription(onEventTrigger$, observer);

  return onEventTrigger;
}

我将传给useEventCallback的函数替换成了usePersistCallback内的observer。

crimx commented 2 years ago

useSubscription 在触发回调前会先检查 staleness ,从而避免调用旧的回调。所以即便在渲染阶段也是调用正确的回调。

https://github.com/crimx/observable-hooks/blob/e29d60b9305ff9c1bc4d7ac225147ec2de6349be/packages/observable-hooks/src/internal/use-subscription-internal.ts#L38-L40

dragooncjw commented 2 years ago

其实我是通过useSubscription的observer来实现的,这里看源码是通过useRef实现的。如果我使用input$来实现我想要实现的功能的话,会因为闭包,导致我在函数内部调用的永远是init时候的值。 https://github.com/crimx/observable-hooks/blob/e29d60b9305ff9c1bc4d7ac225147ec2de6349be/packages/observable-hooks/src/internal/use-subscription-internal.ts#L42-L47

crimx commented 2 years ago

可以通过 codesandbox 构造一个例子说明你遇到的问题么?这里没看明白与 RxJS 有什么关系?

dragooncjw commented 2 years ago

其实是我想要接触一下React 18的并发模式,但是项目中又有使用React useEventCallback的场景,(https://reactjs.org/docs/hooks-faq.html) 因此想要找到一种方法,避免useEventCallback实现中的useRef导致的并发模式下的问题。我理解RxJS的订阅发布机制可以解决这个问题,但是如果我使用input$来实现,这就意味着我需要将所有依赖的数据全部转化为Observable,这其实并不符合预期,且工作量过大。 下面是React官网 useEventCallback的实现源码:

function useEventCallback(fn, dependencies) {
  const ref = useRef(() => {
    throw new Error('Cannot call an event handler while rendering.');
  });

  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}

由于使用useRef记住了fn,因此fn会更新,但是useEventCallback包裹的fn的地址并不会更新。 但是这样的做法React官方不推荐,官网上是这么讲的: image 我想找个替代方案,是否可以通过observable-hooks实现一套新的useEventCallback,可以使我们记录函数内部最新的状态,同时遵从React官方文档的意愿,避免使用useEventCallback

dragooncjw commented 2 years ago

我简单写了一个codesandbox解释我使用input$会存在的问题: https://codesandbox.io/s/context-with-and-without-usememo-with-memo-forked-u26d3?file=/src/ProviderWithMemo.js 我的ProviderWithMemo组件中点击Increment按钮的时候,console内打印出来的在App.js内的值永远是0,而ProviderWithoutMemo内的值永远是最新的.

crimx commented 2 years ago

这是因为你的 test 放在了 useObservableCallbackinit 回调中,文档已经说明 init 回调只会执行一遍。另外见注意事项

  const [onChangeTest, textChange$] = useObservableCallback((event$) =>
    event$.pipe(
      tap(() => {
        test("withMemo");
        setCounter((c) => c + 1);
      })
    )
  );

tap 是反 Rx 响应式模式所以一遍不推荐使用。

副作用放在 useSubscription 即可。

import { useObservableCallback, useSubscription, identity } from "observable-hooks";

  const [onChangeTest, textChange$] = useObservableCallback(identity);

  useSubscription(textChange$, () => {
    test("withMemo");
    setCounter((c) => c + 1);
  });
dragooncjw commented 2 years ago

是的,我一开始的问题里也写了我是使用了useSubscription里的observer实现解决的,但是问题在于我发现useSubscription源码里对observer内数据的同步还是通过使用useRef记录的,其实原理和使用useEventCallback是一样的,所以感觉使用您上一个回复中的方法并不能避免React官方文档中useEventCallback可能会出现的问题。

crimx commented 2 years ago

啊,是我记错了。之前举例的代码是用于检查 observable 更新的 staleness,不是检查回调的 staleness。

这个确实是与 useEventCallback 一样的,之前尝试过在 render 阶段赋值给 ref 但是因为有副作用会导致 React devtools 出错。

这个在 Observable 中可以接受是因为业务上限制了这个可能性。useSubscription 用于副作用的时候对于这个时机要求不敏感,用于 state 的时候因为 setState 都是一样的所以也不会出现问题。

dragooncjw commented 2 years ago

以后observable-hooks会推出相关的解决方案吗?我正在寻求useEventCallback的替代方案,但是没有找到好的解决方法

crimx commented 2 years ago

React 选择走上了函数组件+并行模式这条路决定了它不能在渲染阶段写带副作用的代码,所以 useEventCallback 这种模式只能通过业务设计上来规避,要么保证回调不会在渲染阶段触发,要么回调中不处理强依赖上下文的业务。而两种情况都需要的业务也非常少,通常都是反 React 模式的使用方式。

React 团队其实也知道这么结合代码不好写,所以拖了这么久 18 也没全上并行模式。

dragooncjw commented 2 years ago

感谢大佬指点