【问题标题】:how to avoid useEffect() race condition如何避免 useEffect() 竞争条件
【发布时间】:2020-09-23 09:24:20
【问题描述】:

我对 ReactJS 非常熟悉,但刚刚开始接触 Hooks。

有时对我来说困难的是如何将命令式 API 封装到钩子中。我感觉有时我使用了错误的方法。

例如,从现实世界的应用程序中获取这个大大简化的示例。假设有一个全局状态 API(不能更改):

  • server.getState(name) 以同步方式返回最后一个已知状态(假设是一个字符串)
  • server.on(name, callback) 为该状态的更改注册一个事件处理程序
  • server.off(name, callback) 取消注册该事件处理程序

我想创建一个自定义挂钩 useServerState(name, defaultValue),它返回该状态的实时值。

我的实现如下所示:

function useServerState(name, defaultValue) {

  const [ curState, setCurState ] = useState(() => server.getState(name) || defaultValue);

  useEffect(
    () => {

      const evHandler = payload => setCurState(payload);

      // register on mount:
      server.on(name, evHandler);

      // de-register on unmount:
      return () => server.off(name, evHandler);

    },
    [ name ]
  );

  return curState;

}

用法:


function SomeComponent() {

  const temperature = useServerState("temp_outside", "unknown");

  return <div>
    Current temperature: {temperature} degrees
  </div>;

}

问题

这可行,但问题是useEffect() 似乎异步调用函数。在我的 useEffect() 处理程序运行之前会有一些延迟,例如 50 毫秒 左右。在此期间我的钩子对于可能同时出现的新事件是盲目的(因为尚未注册事件处理程序),这意味着即使它已经被钩子也可能返回旧状态更新了。

一种(不优雅的)解决方法

我可以通过再次调用 getState() 来解决这个问题在 useEffect() 处理程序中(换句话说,在“mount”上):

function useServerState(name, defaultValue) {

  const getInitialState = () => server.getState(name) || defaultValue;
  const [ curState, setCurState ] = useState(getInitialState);

  useEffect(
    () => {

      const evHandler = payload => setCurState(payload);

      // register on mount:
      server.on(name, evHandler);

// ----->
      // anti race condition hack:
      const nv = getInitialState();
      
      if (nv !== curState)
        setCurState(nv);
// <-----

      // de-register on unmount:
      return () => server.off(name, evHandler);

    },
    [ name ]
  );

  return curState;

}

另一个(不好的)解决方法

另一种解决方案是始终使用固定的起始值并仅在挂载时读取当前状态:

function useServerState(name, defaultValue) {

  const [ curState, setCurState ] = useState(null);

  useEffect(
    () => {

      const evHandler = payload => setCurState(payload);

      // register on mount:
      server.on(name, evHandler);

      // set initial state:
      setCurState(server.getState(name) || defaultValue);

      // de-register on unmount:
      return () => server.off(name, evHandler);

    },
    [ name ]
  );

  return curState;

}

这个解决方案的缺点是它总是会导致两次渲染:一个带有null,然后另一个带有实际状态。这是不希望的。

如何改进?

解决方法对我来说似乎很尴尬,让我想知道是否有更好的方法来做到这一点,而不涉及解决方法。我希望 React 团队已经考虑过这种情况。我错过了什么?

【问题讨论】:

  • 第一个(简单的)解决方案不仅会遇到延迟,而且当name更改时,也不会根据新名称更新状态(重置状态)。如果您问我,您的“不雅”解决方案似乎很合理。唯一的问题是如何实施平等检查。
  • 同意,我错过了这样一个事实,即“不优雅”的解决方案也需要注意名称更改。这样看来,解决方案似乎不那么老套了。
  • 根据服务器的实际状态,甚至可能不需要像react does that for you already 那样进行额外的相等性检查。用相同的值调用useState 的setter 不会触发重新渲染。但是,如果它是一个对象,您需要自己实现相等性检查,因为 react 通过引用比较它们。
  • 我错过了这个细节,谢谢!

标签: javascript reactjs react-hooks


【解决方案1】:

您的原始代码 cmet 没有反映代码逻辑,我相信修复它就足够了:

function useServerState(name, defaultValue) {
  const [currState, setCurState] = useState(null);

  // Register on MOUNT
  useEffect(() => {
    if (!currState) {
      const currServerState = server.getState(name) || defaultValue;
      setCurrState(currServerState);
    }
  }, [name]);

  // Handle name change
  useEffect(() => {
    if (!server.getState(name)) {
      server.on(name, setCurState);
    }
    return () => server.off(name, setCurState);
  }, [name]);

  return currState;
}

【讨论】:

  • 关于“50ms问题”,没有可复现的例子无法详细说明,如果你在codesanbox中添加这样的例子,如果可能的话,我们可以谈谈如何减少这个时间。跨度>
  • 该解决方案也会在装载时渲染两次,一次使用null,一次使用 OP 称为“不需要”的实际状态。
  • 我认为它是“理想的”,因为 server.on 是异步的,您应该有一些 loading 指示符(如 null),这也是每个异步库的默认行为,“A(不优雅)解决方法”对于您提到的“no null”问题已经足够了
  • 是的,您当然可以通过显示加载指示器来解决这个问题。由 OP 决定这是否对他有用。你能向我解释一下第一个效果的好处吗?似乎它订阅了一个事件而从未取消订阅,并将服务器状态传递给 on() 而不是名称。我不确定这是否可能。
  • 是的,你是对的,它没有意义,做了一些改变
猜你喜欢
  • 2019-06-12
  • 1970-01-01
  • 1970-01-01
  • 2015-01-30
  • 2010-09-25
  • 2016-09-20
  • 1970-01-01
  • 2010-09-25
相关资源
最近更新 更多