# 如何在 React Hooks 中获取数据

https://www.robinwieruch.de/react-hooks-fetch-data

在本教程中,我想向您展示如何使用 state 和 effect hooks 在 React 中使用 Hooks 获取数据。 我们将使用广为人知的 Hacker News API (opens new window) 从科技世界获取热门文章。 你还将为数据获取实现自定义 hooks,这些 hooks 可以在您的应用程序中的任何地方重用,也可以作为独立节点包发布在 npm 上。

如果您对这个新的 React 功能一无所知,请查看 React Hooks 简介。 如果您想查看已完成的项目以查看展示如何在 React 中使用 Hooks 获取数据的示例,请查看此 GitHub 存储库 (opens new window)

如果您只想准备好使用 React Hook 来获取数据:npm install use-data-api 并按照文档 (opens new window)进行操作。 如果您使用它,请不要忘记给它加星号:-)

注意:将来,React Hooks 不会用于在 React 中获取数据。 相反,一个名为 Suspense 的功能将负责它。 尽管如此,以下演练还是了解有关 React 中 state 和 effect hooks 的更多信息的好方法。

# 使用 React Hooks 获取数据

如果您不熟悉 React 中的数据获取,请查看我在 React 中的大量数据获取文章 (opens new window)。 它会引导您了解如何使用 React 类组件获取数据、如何使用 Render Prop 组件和高阶组件使其可重用,以及它如何处理错误处理和加载指示器。 在本文中,我想通过函数组件中的 React Hook 向您展示所有这些。

import React, { useState } from "react";

function App() {
  const [data, setData] = useState({ hits: [] });

  return (
    <ul>
      {data.hits.map((item) => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

App 组件显示项目列表(hits = Hacker News 文章)。 状态和状态更新函数来自名为 useState 的状态 hooks,它负责管理我们要为 App 组件获取的数据的本地状态。 初始状态是表示数据的对象中的空列表。 目前还没有人为这些数据设置任何状态。

我们将使用 axios 来获取数据,你也可以使用其他的 http 库或者原生的 fetch。如果你还没有安装 axios,在命令行工具中使用 npm install axios 来安装。然后实现 effect hook 来回去数据:

import React, { useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      "https://hn.algolia.com/api/v1/search?query=redux"
    );

    setData(result.data);
  });

  return (
    <ul>
      {data.hits.map((item) => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

useEffect 用于使用 axios 从 API 中获取数据,并使用 state hook 的更新函数将数据设置在组件的本地状态中。 使用 async/await 完成 promise。

但是,当您运行应用程序时,您应该会遇到一个令人讨厌的循环。 useEffect 在组件挂载时运行,也在组件更新时运行。 因为我们在每次数据获取后设置状态,组件更新并且 effect 再次运行。 它一次又一次地获取数据。 这是一个 bug,需要避免。 我们只想在组件挂载时获取数据。 这就是为什么您可以提供一个空数组作为 useEffect 的第二个参数,以避免在组件更新时激活它,但仅用于安装组件。

import React, { useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      "https://hn.algolia.com/api/v1/search?query=redux"
    );

    setData(result.data);
  }, []);

  return (
    <ul>
      {data.hits.map((item) => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

第二个参数可用于定义 hooks 所依赖的所有变量(分配在此数组中)。 如果其中一个变量发生变化,hooks 会再次运行。 如果包含变量的数组为空,则在更新组件时根本不会运行钩子,因为它不必监视任何变量。

还有最后一个问题。 在代码中,我们使用 async/await 从第三方 API 获取数据。 根据文档,每个用 async 注释的函数都会返回一个隐式 Promise:“ async 函数声明定义了一个异步函数,它返回一个 AsyncFunction 对象。异步函数是一个通过事件循环异步操作的函数,使用隐式 Promise 返回它的结果。”。 但是,effect hooks 不应返回任何内容或返回一个清理函数。 这就是为什么您可能会在开发者控制台日志中看到以下警告: 07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing.。 不支持 Promises 和 useEffect(async () => ...),但您可以在 effect 中调用 async 函数。这就是不允许在 useEffect 函数中直接使用 async 的原因。 让我们通过在 effect 中使用 async 函数来为它实现一个解决方法。

import React, { useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        "https://hn.algolia.com/api/v1/search?query=redux"
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <ul>
      {data.hits.map((item) => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

简而言之,这就是使用 React 钩子获取数据。 但是,如果您对错误处理、加载指示器、如何触发从表单中获取数据以及如何实现可重用的数据获取挂钩感兴趣,请继续阅读。

# 如何以编程方式/手动方式触发钩子?

太好了,一旦组件挂载,我们就会获取数据。 但是如何使用输入字段告诉 API 我们对哪个主题感兴趣呢? “Redux”作为默认查询。 但是关于“React”的话题呢? 让我们实现一个输入元素,使某人能够获取除“Redux”故事之外的其他故事。 因此,为输入元素引入一个新状态。

import React, { Fragment, useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        "https://hn.algolia.com/api/v1/search?query=redux"
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

export default App;

目前,这两个状态彼此独立,但现在您希望将它们耦合以仅获取输入字段中查询指定的文章。 通过以下更改,组件应在安装后按查询词获取所有文章。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, []);

  return (
    ...
  );
}

export default App;

缺少一项:当您尝试在输入字段中输入内容时,在效果触发安装后没有其他数据获取。 那是因为您提供了空数组作为效果的第二个参数。 该效果不依赖于任何变量,因此仅在组件挂载时触发。 但是,现在效果应该取决于查询。 一旦查询更改,数据请求应再次触发。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    ...
  );
}

export default App;

一旦您更改了输入字段中的值,数据的重新获取就应该起作用。 但这带来了另一个问题:在您在输入字段中输入的每个字符上,都会触发效果并执行另一个数据获取请求。 如何提供一个按钮来触发请求,从而手动触发钩子?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");
  const [search, setSearch] = useState("");

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${query}`
      );

      setData(result.data);
    };

    fetchData();
  }, [query]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button type="button" onClick={() => setSearch(query)}>
        Search
      </button>

      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

现在,使 effect 依赖于搜索状态,而不是随输入字段中每次击键而变化的波动查询状态。 一旦用户点击按钮,新的搜索状态被设置并且应该手动触发效果钩子。

...

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [search, setSearch] = useState('redux');

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        `http://hn.algolia.com/api/v1/search?query=${search}`,
      );

      setData(result.data);
    };

    fetchData();
  }, [search]);

  return (
    ...
  );
}

export default App;

此外,搜索状态的初始状态设置为与查询状态相同的状态,因为组件也在装载时获取数据,因此结果应该反映输入字段中的值。 但是,具有类似的查询和搜索状态有点令人困惑。 为什么不将实际 URL 设置为状态而不是搜索状态?

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");
  const [url, setUrl] = useState(
    "https://hn.algolia.com/api/v1/search?query=redux"
  );

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(url);

      setData(result.data);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </Fragment>
  );
}

这就是使用 effect hook 获取隐式编程数据的情况。 您可以决定效果取决于哪个状态。 一旦您在单击或其他副作用中设置此状态,此 effect 将再次运行。 在这种情况下,如果 URL 状态发生变化,则 effect 会再次运行以从 API 获取数据。

# REACT HOOKS 中的加载指示器

让我们为数据获取引入一个加载指示器。 它只是由状态 hook 管理的另一种状态。 加载标志用于在 App 组件中渲染加载指示器。

import React, { Fragment, useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");
  const [url, setUrl] = useState(
    "https://hn.algolia.com/api/v1/search?query=redux"
  );
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);

      const result = await axios(url);

      setData(result.data);
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map((item) => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

一旦调用 effect 进行数据获取,当组件挂载或 URL 状态发生变化时,加载状态将设置为 true。 请求解决后,加载状态再次设置为 false。

# 使用 React Hooks 处理错误

怎样使用 React 钩子获取数据的错误处理? 错误只是用状态 hook 初始化的另一个状态。 一旦出现错误状态,App 组件就可以为用户渲染反馈。 使用 async/await 时,通常使用 try/catch 块进行错误处理。 您可以在 effect 内执行此操作:

import React, { Fragment, useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");
  const [url, setUrl] = useState(
    "https://hn.algolia.com/api/v1/search?query=redux"
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <Fragment>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        Search
      </button>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map((item) => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

每次 hook 再次运行时都会重置错误状态。 这很有用,因为在请求失败后,用户可能想要重试,这应该会重置错误。 为了自己强制执行错误,您可以将 URL 更改为无效的内容。 然后检查错误信息是否出现。

# 通过表单获取数据

获取数据的正确形式怎么样? 到目前为止,我们只有输入字段和按钮的组合。 一旦你引入了更多的输入元素,你可能想用一个表单元素来包装它们。 此外,表单还可以通过键盘上的“Enter”触发按钮。

function App() {
  ...

  return (
    <Fragment>
      <form
        onSubmit={() =>
          setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
        }
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

但是现在浏览器在单击提交按钮时会重新加载,因为这是提交表单时浏览器的原生行为。 为了防止默认行为,我们可以在 React 事件上调用一个函数。 这也是你在 React 类组件中的做法。

function App() {
  ...

  return (
    <Fragment>
      <form onSubmit={event => {
        setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);

        event.preventDefault();
      }}>
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      ...
    </Fragment>
  );
}

现在,当你点击提交按钮时,浏览器将不再重新加载。它的工作方式与前面一样,但这次使用的是表单,而不是简单的输入字段和按钮组合。你也可以按键盘上的“Enter”键。

# 自定义数据获取 hook

为了提取用于数据获取的自定义 hook,将属于数据获取的所有东西移动到它自己的函数中,除了属于输入字段的查询状态,但包括加载指示器和错误处理。还要确保从 App 组件中使用的函数中返回所有必要的变量。

const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    "https://hn.algolia.com/api/v1/search?query=redux"
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return [{ data, isLoading, isError }, setUrl];
};

现在,你的新钩子可以在 App 组件中再次使用了:

function App() {
  const [query, setQuery] = useState("redux");
  const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();

  return (
    <Fragment>
      <form
        onSubmit={(event) => {
          doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={(event) => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
      ...
    </Fragment>
  );
}

初始状态也可以是通用的。将它简单地传递给新的自定义钩子:

import React, { Fragment, useState, useEffect } from "react";
import axios from "axios";

const useDataApi = (initialUrl, initialData) => {
  const [data, setData] = useState(initialData);
  const [url, setUrl] = useState(initialUrl);
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);

      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return [{ data, isLoading, isError }, setUrl];
};

function App() {
  const [query, setQuery] = useState("redux");
  const [{ data, isLoading, isError }, doFetch] = useDataApi(
    "https://hn.algolia.com/api/v1/search?query=redux",
    { hits: [] }
  );

  return (
    <Fragment>
      <form
        onSubmit={(event) => {
          doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);

          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={(event) => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>

      {isError && <div>Something went wrong ...</div>}

      {isLoading ? (
        <div>Loading ...</div>
      ) : (
        <ul>
          {data.hits.map((item) => (
            <li key={item.objectID}>
              <a href={item.url}>{item.title}</a>
            </li>
          ))}
        </ul>
      )}
    </Fragment>
  );
}

export default App;

这就是用自定义钩子获取数据的方法。钩子本身并不知道 API 的任何信息。它从外部接收所有参数,只管理必要的状态,如数据、加载和错误状态。它执行请求并将数据作为自定义数据获取钩子返回给组件。

# 使用 REDUCER HOOK 获取数据

到目前为止,我们已经使用了各种状态钩子来管理数据获取状态、加载状态和错误状态。然而,不知何故,所有这些有自己的状态钩子管理的状态都属于一起,因为它们关心的是同一个原因。如您所见,它们都在数据获取函数中使用。它们属于一起的一个很好的指示是它们被一个接一个地使用(例如,setIsError, setIsLoading)。让我们将它们与 Reducer Hook 结合起来。

Reducer Hook 返回一个状态对象和一个改变状态对象的函数。这个函数——称为 dispatch 函数——接受一个具有类型和可选负载的操作。所有这些信息都用于实际的 reducer 函数,从以前的状态中提取新的状态,动作的可选载荷和类型。让我们看看这是如何在代码中工作的:

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

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...
};

Reducer 钩子以 Reducer 函数和初始状态对象作为参数。在我们的例子中,数据、加载和错误状态的初始状态的参数没有改变,但它们被聚合到一个状态对象,由一个 reducer 钩子管理,而不是单个状态钩子。

const dataFetchReducer = (state, action) => {
  ...
};

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  useEffect(() => {
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });

      try {
        const result = await axios(url);

        dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
      } catch (error) {
        dispatch({ type: 'FETCH_FAILURE' });
      }
    };

    fetchData();
  }, [url]);

  ...
};

现在,在获取数据时,可以使用 dispatch 函数将信息发送给 reducer 函数。使用 dispatch 函数发送的对象具有一个强制性的类型属性和一个可选的有效负载属性。类型告诉 reducer 函数需要应用哪个状态转换 reducer 可以额外使用负载来提取新状态。毕竟,我们只有三种状态转换:初始化取回过程,通知成功的数据取回结果,以及通知错误的数据取回结果。

在自定义钩子的末尾,状态会像以前一样返回,但因为我们有一个状态对象,而不再是独立的状态。这样,调用 useDataApi 的自定义钩子仍然可以访问 dataisLoadingsError:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

  ...

  return [state, setUrl];
};

最后但并非最不重要的是,reducer 函数 的实现是缺失的。它需要处理三个不同的状态转换,分别是 FETCH_INITFETCH_SUCCESSFETCH_FAILURE。每个状态转换都需要返回一个新的状态对象。让我们看看如何使用 switch case 语句来实现:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return { ...state };
    case "FETCH_SUCCESS":
      return { ...state };
    case "FETCH_FAILURE":
      return { ...state };
    default:
      throw new Error();
  }
};

reducer 函数可以通过它的参数访问当前状态和传入的动作。到目前为止,在 out switch case 语句中,每个状态转换只返回以前的状态。解构语句用于保持状态对象不变——这意味着状态永远不会直接改变——以执行最佳实践。现在让我们重写当前状态返回的一些属性,以改变每个状态转换的状态:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case "FETCH_INIT":
      return {
        ...state,
        isLoading: true,
        isError: false,
      };
    case "FETCH_SUCCESS":
      return {
        ...state,
        isLoading: false,
        isError: false,
        data: action.payload,
      };
    case "FETCH_FAILURE":
      return {
        ...state,
        isLoading: false,
        isError: true,
      };
    default:
      throw new Error();
  }
};

现在,每个状态转换(由操作的类型决定)都根据以前的状态和可选的有效负载返回一个新状态。例如,在成功请求的情况下,负载用于设置新状态对象的数据。

总之,Reducer Hook 确保了状态管理的这一部分是用它自己的逻辑封装的。通过提供操作类型和可选的有效负载,您将总是以一个可预测的状态更改结束。此外,您将永远不会遇到无效状态。例如,以前可能会意外地将 isLoading 和 isError 状态设置为 true。在这种情况下,UI 中应该显示什么?现在,reducer 函数定义的每个状态转换都将导致一个有效的状态对象。

# 终止 hook 中的数据获取

在 React 中,一个常见的问题是,即使组件已经卸载(例如,由于使用 React Router 导航离开),组件状态还是被设置了。我以前在这里写过这个问题,描述了如何防止在各种场景中为未挂载的组件设置状态。让我们看看如何防止在自定义钩子中为数据获取设置状态:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });

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

    const fetchData = async () => {
      dispatch({ type: "FETCH_INIT" });

      try {
        const result = await axios(url);

        if (!didCancel) {
          dispatch({ type: "FETCH_SUCCESS", payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: "FETCH_FAILURE" });
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  return [state, setUrl];
};

每个 Effect Hook 都有一个清理函数,当组件卸载时运行。清理函数是从钩子返回的一个函数。在我们的例子中,我们使用一个名为 didCancel 的布尔标志来让我们的数据获取逻辑知道组件的状态(挂载/卸载)。如果组件确实卸载了,该标志应该设置为 true,从而防止在最终异步解析数据获取后设置组件状态。

注意:实际上并不是数据获取被中止——这可以通过 Axios cancel 实现——而是对已卸载的组件不再执行状态转换。因为在我看来 Axios cancel 并不是最好的 API,所以这个布尔标志可以防止设置状态。

# 最后

您已经了解了如何在 React 中使用状态和效果的 React 钩子来获取数据。如果您对使用渲染道具和高阶组件的类组件(和函数组件)中的数据获取感兴趣,请参阅我的另一篇文章。否则,我希望这篇文章对您学习 React hook 以及如何在真实场景中使用它们有所帮助。