原文:How to fetch data with React Hooks?html
In this tutorial, I want to show you how to fetch data in React with Hooks by using the state and effect hooks. We will use the widely known Hacker News API to fetch popular articles from the tech world. You will also implement your custom hook for the data fetching that can be reused anywhere in your application or published on npm as standalone node package.node
本文将会讲述如何在React中经过使用useState和uesEffect来fetch异步获取数据,例子中将使用广为人知的Hacker News API来获取技术领域上的一些受欢迎的文章,同时还会尝试使用自定义Hook来获取数据,以便在开发者的工程或者npm包中来提升复用性。react
If you don't know anything about this new React feature, checkout this introduction to React Hooks. If you want to checkout the finished project for the showcased examples that show how to fetch data in React with Hooks, checkout this GitHub repository.ios
若是你还不太清楚react的Hook新特性,那么建议先去官方文档或者关于hook的介绍查看相关内容,本文的所有完整示例代码都在这个github仓库中git
If you just want to have a ready to go React Hook for data fetching: npm install use-data-api and follow the documentation. Don't forget to star it if you use it :-)github
若是你只是想经过使用Hook来获取数据,能够经过npm install use-data-api
,并配合文档use-data-api文档来使用,若是有帮助的话请➕星;shell
Note: In the future, React Hooks are not be intended for data fetching in React. Instead, a feature called Suspense will be in charge for it. The following walkthrough is nonetheless a great way to learn more about state and effect hooks in React.npm
注意:在未来,React Hooks并不计划一直成为异步获取数据的方式,而是使用之后的新特性 Suspense 来获取数据,不过经过本文的内容依然是一个学习好state和effect相关Hook的不错方法。
If you are not familiar with data fetching in React, checkout my extensive data fetching in React article. It walks you through data fetching with React class components, how it can be made reusable with Render Prop Components and Higher-Order Components, and how it deals with error handling and loading spinners. In this article, I want to show you all of it with React Hooks in function components.redux
若是你对于react中获取数据不太熟悉,能够先查看相关内容进行了解,关于react获取数据文章,文中会讲述怎么在class组件获取数据,并经过Render props和高阶组件的方式来提升复用,同时还有关于错误处理和加载状态的部分,而本文将会说明在react中如何使用Hook来达到上述同样的功能;axios
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;
The App component shows a list of items (hits = Hacker News articles). The state and state update function come from the state hook called useState that is responsible to manage the local state for the data that we are going to fetch for the App component. The initial state is an empty list of hits in an object that represents the data. No one is setting any state for this data yet.
这个App组件展现了一个列表,其中hits是指上面提到的Hacker News articles文章列表,使用useState来返回state和修改state的方法函数,咱们获取的数据就会经过这个方法来设置到组件的state中,初始state就是一个空数组,而且没有调用设置state的方法。
We are going to use axios to fetch data, but it is up to you to use another data fetching library or the native fetch API of the browser. If you haven't installed axios yet, you can do so by on the command line with npm install axios. Then implement your effect hook for the data fetching:
本文将使用 axios 来获取数据,可是这能够根据你本身的习惯来选择其余的库或者直接使用fetch原生API,若是尚未安装 axios,那么可使用npm install axios
来安装,而后使用 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;
The effect hook called useEffect is used to fetch the data with axios from the API and to set the data in the local state of the component with the state hook's update function. The promise resolving happens with async/await.
经过使用 useEffect 来调用 axios 来获取数据,获取数据后经过useState返回的setState方法来修改state,state的变动会驱动DOM的更新渲染;其中使用了 async/await 来请求数据;
However, when you run your application, you should stumble into a nasty loop. The effect hook runs when the component mounts but also when the component updates. Because we are setting the state after every data fetch, the component updates and the effect runs again. It fetches the data again and again. That's a bug and needs to be avoided. We only want to fetch data when the component mounts. That's why you can provide an empty array as second argument to the effect hook to avoid activating it on component updates but only for the mounting of the component.
可是,以上代码运行时会进入一个循环请求数据的状态,useEffect会由于DOM的更新而不断的执行,useEffect中求数据后设置state引发组件更新,组件更新渲染后会执行useEffect从而从新请求数据,再次从新设置state,这将会致使死循环;咱们须要避免这种状况,咱们须要只在第一次挂载组件的时候请求就行了,useEffect方法经过第二个参数【deps 依赖】传入一个空数组来控制,只在组件的挂载过程去执行,其余时间不执行。
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;
The second argument can be used to define all the variables (allocated in this array) on which the hook depends. If one of the variables changes, the hook runs again. If the array with the variables is empty, the hook doesn't run when updating the component at all, because it doesn't have to watch any variables.
useEffect的第二个参数是一个包括了在当前 useEffect 中所使用的的变量的数组,若是其中某个变量变动【Object.is判断】了,那么在最近一次渲染后,这个useEffect会被从新执行;若是传入的是一个空数组,那么除了第一次挂载组件时会执行,其余时间更新组件的时候不会执行,由于不依赖任何变量,也就是依赖不会变动,常常被用于模拟 componentDidMount,可是仍是存在区别的。
There is one last catch. In the code, we are using async/await to fetch data from a third-party API. According to the documentation every function annotated with async returns an implicit promise: "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. ". However, an effect hook should return nothing or a clean up function. That's why you may see the following warning in your developer console log: 07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => ...) are not supported, but you can call an async function inside an effect.. That's why using async directly in the useEffect function isn't allowed. Let's implement a workaround for it, by using the async function inside the effect.
使用空数组后还存在一个问题,咱们使用了 async/await 方式来请求接口,async/await的规范有:async 函数经过事件循环来执行异步操做,返回一个promise对象。这和react规定 useEffect 须要返回一个清除函数或者无返回值这一要求不符,因此运行上面的代码将会在控制台看到一段日志提示:07:41:22.910 index.js:1452 Warning: useEffect function must return a cleanup function or nothing。返回promise或者使用useEffect(async () => ...)形式是不行的,可是能够在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;
That's data fetching with React hooks in a nutshell. But continue reading if you are interested about error handling, loading indicators, how to trigger the data fetching from a form, and how to implement a reusable data fetching hook.
Great, we are fetching data once the component mounts. But what about using an input field to tell the API in which topic we are interested in? "Redux" is taken as default query. But what about topics about "React"? Let's implement an input element to enable someone to fetch other stories than "Redux" stories. Therefore, introduce a new state for the input element.
咱们上面已经完成了在一个组件挂载是发起请求,不过有的时候咱们须要通过和用户交互获取数据来发起请求获取数据,咱们来实践一下经过input表单来肯定咱们须要什么时候请求何种数据。
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;
At the moment, both states are independent from each other, but now you want to couple them to only fetch articles that are specified by the query in the input field. With the following change, the component should fetch all articles by query term once it mounted.
咱们有两个独立的state了,此时但愿可以根据input表单的内容来请求数据,经过下面的修改来将query关联到请求中。
useEffect(() => { const fetchData = async () => { const result = await axios( `http://hn.algolia.com/api/v1/search?query=${query}`, ); setData(result.data); }; fetchData(); }, []);
One piece is missing: When you try to type something into the input field, there is no other data fetching after the mounting triggered from the effect. That's because you have provided the empty array as second argument to the effect. The effect depends on no variables, so it is only triggered when the component mounts. However, now the effect should depend on the query. Once the query changes, the data request should fire again.
还有一个问题须要调整,由于咱们手动修改input的内容时,并不会从新发起请求,这是由于上面提到的useEffect的的依赖是一个空数组,这样在变量变动时,组件更新渲染后并不会再次调用useEffect,因此咱们须要把咱们须要的依赖 query 添加到useEffect的第二个参数依赖中,以下:
useEffect(() => { const fetchData = async () => { const result = await axios( `http://hn.algolia.com/api/v1/search?query=${query}`, ); setData(result.data); }; fetchData(); }, [query]);
The refetching of the data should work once you change the value in the input field. But that opens up another problem: On every character you type into the input field, the effect is triggered and executes another data fetching request. How about providing a button that triggers the request and therefore the hook manually?
以上是的在修改input的内容时会从新请求所需数据,可是每次的修改都会从新请求数据,最好可以经过按钮来控制什么时候进行数据请求;经过修改依赖,依赖于一个通过button被点击后才会修改的state,这样就能够实现想要的结果。
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(); }, [search]); 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> ); }
在react中,一个组件中若是在异步的获取数据过程当中,该组件已经被卸载,那么在异步数据获取到后setState依然会被执行,咱们能够在hook中经过useEffect的返回清除函数来避免这种状况;
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]; };
若是组件卸载的话,会执行didCancel来置为true,这样异步请求 axios 返回的时候,设置state的时候会判断 didCancel 来肯定当前的组件是否已经卸载,这也是js中一种巧妙的闭包的使用。