如何在react hooks中请求数据(译)🦄

3,738 阅读6分钟

在本教程中,我会向您展示如何使用stateeffect在 React with Hooks 中获取数据。我们将使用Hacker News API从科技界获取热门文章。您还将实现用于数据获取的自定义 hook,该 hook 可在应用程序中的任何位置重用或作为独立节点程序包发布在 npm 上。

如果您对这个新的 React 功能一无所知,请查看React Hooks 简介


使用 react hook 请求数据

如果您不熟悉 React 中的数据获取,请查看我的这篇文章。 它会带您了解如何使用 React 类组件获取数据,如何使用Render Prop ComponentsHigher-Order Components,使其可重用,以及如何处理错误和处理 loading 状态。

在本文中,我想通过 函数式组件中的 React Hooks 向您展示这些内容。

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 组件中,显示了一个 list(hits= Hacker News 文章)。 状态和状态更新功能来自名为useState的 hook,该 hook 负责管理 App 组件获取的数据的本地状态。 初始状态是空白列表。

我们将使用 axios 来获取数据,但是由您决定使用另一个数据获取库或浏览器的本机获取 API。 如果尚未安装 axios,则可以在命令行上使用 npm install axios 进行安装。 然后为数据获取实现效果挂钩:

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

export default (props) => {
  const {} = props;
  const [data, setData] = useState({ hits: [] });

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

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

使用 useEffect 这个钩子函数从 API 获取数据,使用 useState 这个钩子函数将获得数据,存储为 state。

然而,在运行代码时,代码会陷入一个讨厌的循环。useEffect 函数在组件安装时执行,但在组件更新时也会执行

因为我们在每次获取数据后都会设置状态(state),所以组件会更新。这就会导致 useEffect 函数再次执行。 它一次又一次地获取数据。

那么怎么避免这个 bug 呢--我们只需要在组件安装时获取数据。 可以为 useEffect 提供一个空数组作为第二个参数,以避免在组件更新时激活它,而仅在安装组件时激活它。

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

export default (props) => {
  const {} = props;
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await fetch(
      "https://hn.algolia.com/api/v1/search?query=redux"
    );
    const resOjson = await result.json();
    setData(resOjson);
  }, []);

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

代码会出现如下警告

Warning: An effect function must not return anything besides a function, which is used for clean-up.

It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately:

useEffect(() => {  async function fetchData() {    // You can await here    const response = await MyAPI.getData(someId);    // ...  }  fetchData(); }, [someId]); // Or [] if effect doesn't need props or state

Learn more about data fetching with Hooks: fb.me/react-hooks…    in Unknown (created by Hello)    in div (created by Hello)    in Hello

useEffect 的第二个参数是一个数组类型的变量,这个变量表示 useEffct 所依赖的所有变量集合。如果数组中任意一个变量变化,则 useEffect 会再次执行。如果变量为空数组,则在组件更新时 useEffect 不会再次执行,因为他不必依赖任何变量

在代码中,我们使用 async / await 从第三方 API 获取数据, 根据 MDN 文档-async,描述

The async function declaration defines an asynchronous function, which returns an AsyncFunction object. An asynchronous function is a function which operates asynchronously via the event loop, using an implicit Promise to return its result.

解释:

async function  用来定义一个返回  AsyncFunction  对象的异步函数,异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的  Promise  返回其结果

但是我们的代码中,异步 useEffect 函数没有返回任何内容,这就是为什么在控制台中会出现上面加粗红色的警告内容--

useEffect 函数必须返回清除函数,或者不返回任何内容。如果你想将 useEffect 写成异步函数,或者返回一个 Promise, 你可以在  useEffect 中 再申明一个异步函数并立刻调用它

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

export default (props) => {
  const {} = props;
  const [data, setData] = useState({ hits: [] });

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

    fetchData();
  }, []);

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

以上就是在 react 的函数式组件中获取数据的方法了,但是,如果你对 如何在 hooks 组件中处理 请求错误,处理 loading 状态,如何从表单中获取数据 以及 如何重用 hooks 函数组件感兴趣,请继续阅读


如何触发 hooks 函数

我们已经可以在一个 hooks 函数中 处理数据请求的情况了,但是怎么给请求传递参数呢? 让我们来实现一个带输入框,并给接口 传递参数的组件

我们通过 (data,query)这两种状态,实现输入框改变的时候查询指定的数据(给 useEffect 函数传递第二个参数来实现这个功能 -- react 官方 useEffect 文档)

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

export default (props) => {
  const {} = props;
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");

  useEffect(() => {
    const fetchData = async () => {
      const result = await fetch(
        `https://hn.algolia.com/api/v1/search?query=${query}`
      );
      const resOjson = await result.json();
      setData(resOjson);
    };

    fetchData();
  }, [query]);

  return (
    <>
      <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>
    </>
  );
};

更改输入框的值后,就可以重新请求数据了。 但这带来了另一个问题:在输入框中每改变一个字符,都会触发数据请求。

输入框频繁触发请求-解决方式一

提供一个触发请求的按钮,从而手动触发钩子?

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

export default (props) => {
  const {} = props;
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState("redux");
  const [search, setSearch] = useState("redux");

  useEffect(() => {
    const fetchData = async () => {
      const result = await fetch(
        `https://hn.algolia.com/api/v1/search?query=${search}`
      );
      const resOjson = await result.json();
      setData(resOjson);
    };

    fetchData();
  }, [search]);

  return (
    <>
      <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>
    </>
  );
};

search的初始状态也被设置为与query相同的状态--因为该组件还需要在第一次记加载完成时获取数据。 但是,具有类似的 search 和 query 状态有点令人困惑。 为什么不将实际 URL 设置为状态来替换 search

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

export default (props) => {
  const {} = props;
  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 fetch(url);
      const resOjson = await result.json();
      setData(resOjson);
    };

    fetchData();
  }, [url]);

  return (
    <>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`https://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>
    </>
  );
};

输入框频繁触发请求-解决方式二

使用节流函数处理


在 hooks 组件请求中使用 loading 状态

大多数场景中,数据处于请求中时应该出现一个加载的状态 。 在 hooks 组件中,这个加载状态也可以存储为一个 state。

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

export default (props) => {
  const {} = props;
  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 fetch(url);
      const resOjson = await result.json();
      setData(resOjson);
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  return (
    <>
      <input
        type="text"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      <button
        type="button"
        onClick={() =>
          setUrl(`https://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>
      )}
    </>
  );
};

在 React Hooks 中捕获错误

当请求出现错误时,在 react hooks 中该如何处理呢? ---- 一旦出现错误,App 组件应立即为用户提供反馈。 我们通常使用 try / catch 块进行错误处理。

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

export default (props) => {
  const {} = props;
  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(null);

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

      try {
        const result = await fetch(url);
        const resOjson = await result.json();
        // 故意写错🤣
        a = a + f;
        setData(resOjson);
      } catch (error) {
        setIsError(`${error}`);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  const renderList = () => {
    if (isError) {
      return <div>{isError}</div>;
    }
    if (isLoading) {
      return <div>Loading ...</div>;
    }
    return (
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    );
  };

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

      {renderList()}
    </>
  );
};

每次useEffect运行时,都会重置错误状态。 这很有用,因为在失败的请求之后,用户可能想要再次尝试,这将重置错误。 您可以将 URL 更改为无效的内容。 然后检查是否显示错误消息。


在 React Form 中请求数据

在表单中如何获取数据呢?到目前为止,我们只有输入字段和按钮的组合。 引入更多输入元素后,您可能需要用 form 元素包裹它们。 而且,form 表单还可以通过键盘上的“ Enter”来触发提交操作。

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

export default (props) => {
  const {} = props;
  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(null);

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

      try {
        const result = await fetch(url);
        const resOjson = await result.json();
        // a = a + f;
        setData(resOjson);
      } catch (error) {
        setIsError(`${error}`);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

  const renderList = () => {
    if (isError) {
      return <div>{isError}</div>;
    }
    if (isLoading) {
      return <div>Loading ...</div>;
    }
    return (
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    );
  };

  return (
    <>
      <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>
      {renderList()}
    </>
  );
};

但是现在,单击“提交”按钮时,浏览器将重新加载,因为这是浏览器提交表单时的原生行为。 为了阻止默认提交,我们可以在 React 事件上调用一个函数。

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

用 Hooks 自定义请求

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

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

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

      try {
        const result = await fetch(url);
        const resOjson = await result.json();
        setData(resOjson);
      } catch (error) {
        setIsError(`${error}`);
      }

      setIsLoading(false);
    };

    fetchData();
  }, [url]);

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

export default useDataApi;
import React, { useState, useEffect, useReducer } from "react";
import useDataApi from "./useDataApi";

export default (props) => {
  const {} = props;

  const [query, setQuery] = useState("redux");

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

  const renderList = () => {
    if (isError) {
      return <div>{isError}</div>;
    }
    if (isLoading) {
      return <div>Loading ...</div>;
    }
    return (
      <ul>
        {data.hits.map((item) => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    );
  };

  return (
    <>
      <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>
      {renderList()}
    </>
  );
};

自定义数据请求的 Hooks 已完成。 自定义的请求 hooks 本身对 API 一无所知。 它从外部接收所有参数,并且仅管理必要的状态,例如数据,加载和错误状态。 它执行请求,并将数据用作自定义数据获,然后将数据返回给组件。


在 Hooks 中终止请求

在 React 中,一个常见的问题是即使组件已经被卸载(例如,由于使用 React Router 导航),也会设置组件状态。 我之前在这里已经写过关于此问题的文章它描述了如何在各种情况下防止未安装组件的设置 state。 让我们看看如何防止在自定义钩子中为数据获取设置状态:

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

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

  useEffect(() => {
    // 主要是这个变量
    let didCancel = false;

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

      try {
        const result = await fetch(url);
        const resOjson = await result.json();
        if (!didCancel) {
          setData(resOjson);
        }
      } catch (error) {
        if (!didCancel) {
          setIsError(`${error}`);
        }
      }

      setIsLoading(false);
    };

    fetchData();

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

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