如何使用React Hooks获取数据时避免竞赛条件

82 阅读3分钟

大约一个月前,我在Twitter上发布了一个使用React Hooks获取数据的例子。虽然这个例子的初衷是好的,但Dan Abromov(React核心团队的)让我知道,我的实现包含了一个竞赛条件。因此,我承诺要写一篇博文,纠正我的实现。这就是那篇博文!

设置

在我们的例子中,当人们的名字被点击时,我们将假意加载他们的资料数据。为了帮助可视化竞赛条件,我们将创建一个fakeFetch 函数,实现0到5秒的随机延迟。

const fakeFetch = (person) => {
  return new Promise((res) => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};

初始实现

我们的初始实现将使用按钮来设置当前的个人资料。我们达到useState 钩子来实现这一点,维护以下状态。

  • person, 用户选择的人
  • data, 根据所选的人,从我们的假取件中加载的数据
  • loading, 当前是否正在加载数据

我们另外使用useEffect 钩子,每当person 变化时,它就会执行我们的假取款。

import React, { Fragment, useState, useEffect } from 'react';

const fakeFetch = (person) => {
  return new Promise((res) => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};

const App = () => {
  const [data, setData] = useState('');
  const [loading, setLoading] = useState(false);
  const [person, setPerson] = useState(null);

  useEffect(() => {
    setLoading(true);
    fakeFetch(person).then((data) => {
      setData(data);
      setLoading(false);
    });
  }, [person]);

  return (
    <Fragment>
      <button onClick={() => setPerson('Nick')}>Nick's Profile</button>
      <button onClick={() => setPerson('Deb')}>Deb's Profile</button>
      <button onClick={() => setPerson('Joe')}>Joe's Profile</button>
      {person && (
        <Fragment>
          <h1>{person}</h1>
          <p>{loading ? 'Loading...' : data}</p>
        </Fragment>
      )}
    </Fragment>
  );
};
export default App;

如果我们运行我们的应用程序并点击其中一个按钮,我们的假取数就会按照预期加载数据。

遇到竞赛条件

当我们开始在人与人之间快速切换时,麻烦就来了。鉴于我们的假取款有一个随机的延迟,我们很快发现我们的取款结果可能会不按顺序返回。此外,我们选择的资料和加载的数据也可能不同步。这是一个不好的现象!

image.png

这里发生的事情是相对直观的:useEffect 钩子中的setData(data) 只在fakeFetch 承诺被解决后才被调用。无论哪个承诺最后解决,都会最后调用setData ,而不管哪个按钮实际上最后被调用。

取消之前的提取

我们可以通过 "取消 "对任何非最近的点击的setData 调用来解决这个竞赛条件。我们通过在useEffect 钩子中创建一个布尔变量,并从useEffect 钩子中返回一个清理函数,将这个布尔 "canceled "变量设置为true 。当承诺解决时,只有当 "canceled "变量为假时,setData 才会被调用。

如果这个描述有点令人困惑,下面的useEffect 钩子的代码示例应该会有所帮助。

useEffect(() => {
  let canceled = false;

  setLoading(true);
  fakeFetch(person).then((data) => {
    if (!canceled) {
      setData(data);
      setLoading(false);
    }
  });

  return () => (canceled = true);
}, [person]);

即使之前的按钮点击的fakeFetch 承诺后来解决了,它的canceled 变量将被设置为true ,而setData(data) 将不会被执行!

让我们来看看我们的新应用程序是如何运作的。

image.png

完美--无论我们点击不同的按钮多少次,我们将永远只看到与最后一次点击按钮相关的数据。

完整的代码

这篇博文的完整代码可以在下面找到。

import React, { Fragment, useState, useEffect } from 'react';

const fakeFetch = (person) => {
  return new Promise((res) => {
    setTimeout(() => res(`${person}'s data`), Math.random() * 5000);
  });
};

const App = () => {
  const [data, setData] = useState('');
  const [loading, setLoading] = useState(false);
  const [person, setPerson] = useState(null);

  useEffect(() => {
    let canceled = false;

    setLoading(true);
    fakeFetch(person).then((data) => {
      if (!canceled) {
        setData(data);
        setLoading(false);
      }
    });

    return () => (canceled = true);
  }, [person]);

  return (
    <Fragment>
      <button onClick={() => setPerson('Nick')}>Nick's Profile</button>
      <button onClick={() => setPerson('Deb')}>Deb's Profile</button>
      <button onClick={() => setPerson('Joe')}>Joe's Profile</button>
      {person && (
        <Fragment>
          <h1>{person}</h1>
          <p>{loading ? 'Loading...' : data}</p>
        </Fragment>
      )}
    </Fragment>
  );
};
export default App;