如何避免 useEffect() 竞争条件

Udo*_*o G 5 javascript reactjs react-hooks

我对 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;

}
Run Code Online (Sandbox Code Playgroud)

用法:


function SomeComponent() {

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

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

}
Run Code Online (Sandbox Code Playgroud)

问题

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

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

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


function SomeComponent() {

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

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

}
Run Code Online (Sandbox Code Playgroud)

另一个(坏的)解决方法

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

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;

}
Run Code Online (Sandbox Code Playgroud)

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

如何提高?

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

Den*_*ash 0

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

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;
}
Run Code Online (Sandbox Code Playgroud)