如何使用React Hooks获取数据?

2,282 阅读15分钟

复读前言

原文链接

这是我在《the Road to React》的作者Robin Wieruchu的博客中看到的一篇关于如何使用React Hooks获取数据的一篇文章,之所以想翻译,是觉得这篇文章可以循序渐进地为读者介绍,在不同的场景下如何使用正确的React Hooks,如果您对React Hooks不是很了解,相信读完这篇文章会对其有新的认识,好吧,闲话不多说,接下来,翻译正文开始。


在本教程中,我将向您展示如何使用React Hooks中的state以及effect来获取数据。我们将使用广为人知的Hacker News API从科技界获取热门文章。你还将实现用于数据获取的自定义钩子函数,该钩子函数可在程序中的任何位置复用,或者作为独立的包发布在npm上。

如果您对这个新的React功能一无所知,请查看此React Hooks简介。如果您想查看项目代码,想知道如何使用React Hooks获取数据,可以查看这个代码仓库

如果您只想使用React Hooks进行数据获取操作,可以执行npm install use-data-api,请按照文档进行操作。如果使用它,别忘了给个Star哦。

使用React Hooks进行数据获取

如果您不熟悉React中的数据获取,请查看我的 extensive data fetching in React article。它带您逐步了解如何使用React类组件获取数据,如何通过Render Prop 组件高阶组件使其变为可复用组件,以及如何进行error处理以及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组件显示项目列表(hits = Hacker News文章)。 通过useState,可以查看state或者更新state,从而管理App组件所获取的数据,初始状态中{ hits: [] }代表一个空列表,此时还未进行数据获取。

我们将使用axios来获取数据,当然使用其他第三方库也是可以的。如果尚未安装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提取数据,并使用useState中的update函数来更新状态,从而更新视图,promise通过async/await来处理。

但是,当您运行应用程序时,应该陷入一个讨厌的循环。useEffect在组件挂载阶段运行,同时也在组件update阶段运行。因为我们在每次获取数据后都要更新state,此时组件会update,那useEffect又会执行,从而又进行state更新(ps:首次加载-> useEffect -> 更新state -> update阶段再次执行useEffect -> 再次更新state......无限循环)。它一次又一次地获取数据,要避免这种错误。我们只想在组件挂载时获取数据。因此,您可以为效果挂钩提供一个空数组[]作为第二个参数,以避免在组件更新时调用,而仅在组件挂载阶段调用它。

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第二个参数可用于定义挂钩所依赖的所有变量(在此数组中分配)。如果变量之一更改,则挂钩再次运行。如果带有变量的数组为空,则在更新组件时此钩子函数根本不会运行,因为它不必监视任何变量。

最后一招。在代码中,我们使用async / await从第三方API获取数据。根据文档,每个带有async注释的函数都会返回一个隐含的promise,但是,useEffect应不返回任何内容返回一个清除函数。这就是为什么您可能在开发人员控制台日志中看到以下警告:useEffect函数必须返回清除函数,否则不返回任何内容。不支持Promises和useEffect(async()=> ...),但是您可以在useEffect内部调用异步函数。这就是为什么useEffect不允许直接在函数中使用异步的原因。让我们通过使用useEffect内的异步函数来实现此变通方法。

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 Hooks获取数据。但是如果您对error处理,loading指示器,如何触发从表单中获取数据以及如何实现可复用的数据获取钩子函数感兴趣,请继续阅读。

如何手动触发hooks?

太好了,一旦组件挂载完毕我们就获取数据。但是,如何使用输入字段来告诉API我们感兴趣的主题呢?“ Redux”被用作默认查询。但是关于“React”的话题呢?让我们实现一个input元素,使我们能够获取“ Redux”文章以外的其他文章。因此,为输入元素引入新的state。

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}`, // 添加query
      );
      setData(result.data);
    };
    fetchData();
  }, []);
  return (
    ...
  );
}
export default App;

还缺少一件事:当您尝试在输入框中输入内容时,从useEffect触发之后,就不会再获取其他数据。那是因为您提供了空数组[]作为效果的第二个参数。该副作用不依赖任何变量,因此仅在挂载组件时触发。但是,现在效果应取决于query的内容。query内容更改后,数据请求应再次触发。

...
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]); // 这样useEffect就会在query发生改变时调用
  return (
    ...
  );
}
export default App;

更改输入内容后,数据应该会重新获取。但这带来了另一个问题:在输入框键入每个字符时,都会触发useEffect并执行另一个数据获取请求。如何提供一个触发请求的按钮,从而手动触发钩子?

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>
  );

现在,使useEffect取决于search状态,而不是随输入内容而变化的波动query状态。用户单击按钮后,便会设置新的search状态,并应手动触发副作用钩子函数。


...
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]); // 此时副作用钩子函数依赖search状态,点击时会触发副作用钩子函数。
  return (
    ...
  );
}
export default App;

search的初始状态也被设置为与query状态相同的数据,该组件在挂载阶段获取数据,因此query反映输入字段中的值。但是,具有类似的query和search状态有点令人困惑。为什么不将实际URL设置为状态而是search状态?

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>
  );
}

如果是使用useEffect进行隐式编程数据获取的话,您可以决定副作用取决于哪个状态,在单击或其他副作用上设置此状态后,该effect hook将再次运行。在这种情况下,如果URL状态发生更改,则effect hook将再次运行请求API以获取数据。

使用React Hooks实现loading指示器

让我们为数据获取引入一个loading指示器。这只是由state hook管理的另一个状态。loading标识用于在App组件中呈现loading指示器。

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); // 初始设置loading状态为false
  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true); // 初始设置loading状态为true
      const result = await axios(url);
      setData(result.data);
      setIsLoading(false); // 数据返回后设置loading状态为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>
      // 通过判断loading状态,来决定是否展示loading样式
      {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;

一旦调用了副作用钩子函数以进行数据获取(在组件挂载或URL状态更改时发生),则loading状态将设置为true。请求返回结果后,loading状态将再次设置为false。

使用React Hooks进行错误处理

使用React Hooks获取数据的错误处理怎么样?该错误只是使用state hook初始化的另一个状态。一旦出现错误状态,App组件即可为用户提供反馈。使用async/await时,通常使用try / catch块进行错误处理。您可以在副作用钩子函数范围内做到这一点:

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);    // 请求开始设置error状态为false
      setIsLoading(true);
      try {
        const result = await axios(url);
        setData(result.data);
      } catch (error) {
        setIsError(true); // 如果catch到错误,即设置error状态为true
      }
      setIsLoading(false); // 最终loading状态设置为false,防止阻塞UI
    };
    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>} // error状态下展示相应UI
      {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;

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

使用Form表单和React Hooks获取数据

如何用适当的形式来获取数据呢?到目前为止,我们只有输入框和按钮的组合。引入更多input框后,您可能需要用form表单包装它们。此外,还可以通过form使用键盘上的“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获取数据

为了抽出获取数据的自定义钩子函数,请将属于数据获取的所有内容(属于输入字段的search状态(还包括loading指示器和error处理))移至其自身的功能。另外,请确保您从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');
  // 封装了函数,通过数组解构直接返回数据获取函数、最终的数据、loading、error这些状态
  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';
// 通过参数传入初始化url和初始化数据,使得数据获取的自定义钩子函数更纯
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一无所知。它从外部接收所有参数,并且仅管理必要的状态,例如数据,loading和error状态。它执行请求并通过数据获取的自定义钩子函数,将数据返回给组件。

通过Reducer Hook进行数据获取

到目前为止,我们已经使用各种状态钩子函数来管理数据的获取状态,loading和error状态。但是,所有的这些状态,管理自己的state hook,它们属于同一类,因为它们关心同一件事情,如您所见,它们都在数据获取函数中使用。一个好的指示器可以将一个又一个的state集结起来(例如:数据、loading、error状态),让我们将它们全部三个与一个Reducer Hook结合使用。

Reducer Hook向我们返回一个state对象和一个更改state对象的函数。该函数--称为disapatch,它采用action(拥有一个type和可选的payload)。所有这些信息都在实际的reducer功能中使用,以从先前state,通过操作的type和可选payload和中提取新state。让我们看看这在代码中是如何工作的:

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);
  // useReducer传入一个type和一个payload对象,通过数组结构获得state和dispatch
  const [state, dispatch] = useReducer(dataFetchReducer, { 
    isLoading: false,
    isError: false,
    data: initialData,
  });
  ...
};

Reducer hook将一个reducer函数以及一个初始化的state对象作为参数。在我们的例子中,数据,loading状态和error状态的初始状态的参数并没有改变,但是它们已经聚合到一个由reducer钩子函数(useReducer)而不是单个useState管理的状态对象中。

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函数发送的action对象具有必填type属性和可选payload属性。该类型告诉reducer函数需要应用哪个状态转换,并且reducer可以额外使用payload属性来提取新state对象。毕竟,我们只有三个状态转换:初始化获取过程,通知成功的数据获取结果,以及通知错误的数据获取结果。

在自定义钩子函数的末尾,state将像以前一样返回,但是因为我们有一个state对象,而不再是独立state。这样一来,谁调用了一个useDataApi自定义的钩子函数仍然得到访问data,isLoading以及isError:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
  ...
  return [state, setUrl]; // 最终返回的state包含data、isError、isLoading
};

最后一点,也是相当重要的一点,我们还缺少reducer函数的实现。它需要根据action对象中三种不同的type(FETCH_INIT,FETCH_SUCCESS和FETCH_FAILURE),来进行三种不同的状态转换,每一次转换都需要返回一个新的state对象,让我们看看如何用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函数可以通过其参数访问state和action。到目前为止,在switch case语句中,每个状态转换仅返回先前的状态。对象解构可以让state对象保持不变-意味着state永远不会直接发生变化-来执行最佳实践。现在,让我们重写一些当前state返回的属性,以在每次状态转换时更改state:

const dataFetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT':
      return {
        ...state, // 解构让我们可以保持原先state不变
        isLoading: true, // 只有当我们手动重写state中的特定属性时,才会发生改变,这样很安全
        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();
  }
};

现在,由action对象中的type属性决定的每个状态转换都会根据先前state和可选payload返回一个新的状态。例如,在成功请求的情况下,payload用于设置新state对象的数据。

总之,Reducer Hook确保状态管理的这一部分使用其自己的逻辑进行封装。通过提供action对象中的type和可选的payload,您将始终返回一个新的数据实现状态变更。此外,您将永远不会陷入无效状态。例如,以前可能会意外地将isLoadingand isError状态同时设置为true。在这种情况下,UI中应显示什么?现在,由reducer函数定义的每个状态转换声明都衍生出一个有效的state对象。

中止Effect Hook的数据获取

在React中,一个常见的问题是即使已卸载组件,会被设置state(例如,由于使用React Router导航)。在此之前,我已经写过有关此问题的文章,它描述了如何防止已卸载组件设置state。让我们看看如何防止在自定义钩子函数中,在已卸载阶段仍然设置state:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData,
  });
  useEffect(() => {
    let didCancel = false; // useEffect执行时设置初始化didCancel为false
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' });
      try {
        const result = await axios(url);
        if (!didCancel) { // 只有在didCancel为false的情况下,才会进行状态变更
          dispatch({ type: 'FETCH_SUCCESS', payload: result.data });
        }
      } catch (error) {
        if (!didCancel) { // 同上
          dispatch({ type: 'FETCH_FAILURE' });
        }
      }
    };
    fetchData();
    return () => {
    // 重点:在useEffect中,如果最终返回一个清除函数,即该函数会在组件卸载阶段执行, 防止组件卸载后仍然设置状态
      didCancel = true; 
    };
  }, [url]);
  return [state, setUrl];
};

每个Effect Hook都带有清理功能,该功能在卸载组件时运行。清理函数是从钩子函数中返回的一个函数。在我们的例子中,我们使用一个boolean标识didCancel来使我们的数据获取逻辑知道组件的state(挂载/卸载)。如果确实卸载了组件,则应该设置该标识true,以防止在最终异步地解析了数据获取之后设置组件state。

注意:实际上,数据获取不会中止-这可以通过Axios Cancellation来实现-但已卸载的组件不再执行状态转换。由于Axios Cancellation在我眼中并不是最好的API,因此该防止设置状态的boolean标识也可以完成这项工作。

您已经了解了state和effect的React钩子函数如何在React中用于数据获取。如果您对使用render props和higher-order components(高阶组件)在类组件(和函数组件)中的数据获取感到好奇,请从头开始阅读我的其他文章。在此,我希望本文对您了解React Hooks以及如何在实际场景中使用它们很有帮助。


复读结语

到这里也算翻译完成了,希望阅读本文后,你可以对React Hooks有一个新的认识,如果你有觉得非常优秀的英文前端文章,希望可以翻译成中文的话,可以在留言评论,我会帮忙翻译一些大家都比较感兴趣的文章。