原文连接: www.robinwieruch.de/react-hooks…javascript
在本教程中,我想经过state和effect hook来像你展现如何用React Hooks来获取数据。我将会使用Hacker News的API来获取热门的技术文章。你将会实现一个属于你本身的自定义hook来在你程序的任何地方复用,或者是做为一个npm包发布出来。java
若是你还不知道这个React的新特性,那么点击React Hooks介绍,若是你想直接查看最后的实现效果,请点击这个github仓库。react
注意:在将来,React Hooks将不会用于React的数据获取,一个叫作Suspense的特性将会去负责它。但下面的教程仍会让你去更多的了解关于React中的state和effect hook。ios
若是你对在React中获取数据还不熟悉,能够查看我其余的React获取数据的文章。它将会引导你经过使用React的class组件来获取数据,而且还能够和render props或者高阶组件一块儿使用,以及结合错误处理和加载状态。在这篇文章中,我将会在function组件中使用React Hooks来展现这些功能。git
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 文章)。state和state的更新函数来自于state hook中useState的调用,它负责管理咱们用来渲染list数据的本地状态,初始状态是一个空数组,此时尚未为其设置任何的状态。github
咱们将使用axios来获取数据,固然你也可使用其余的库或者fetch API,若是你还没安装axios,你能够在命令行使用npm install axios
来安装它。而后来实现用于数据获取的effect hook:npm
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://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;
复制代码
经过axios在useEffect中获取数据,而后经过setData将数据放到组件本地的state中,并经过async/await来处理Promise。编程
然而当你运行程序的时候,你应该会遇到一个讨厌的循环。effect hook不只在组件mount的时候也会在update的时候运行。由于咱们在每一次的数据获取以后,会去经过setState设置状态,这时候组件update而后effect就会运行一遍,这就形成了数据一次又一次的获取。咱们仅仅是想要在组件mount的时候来获取一次数据,这就是为何咱们须要在useEffect的第二个参数提供一个空数组,从而实现只在mount的时候触发数据获取而不是每一次update。redux
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(async () => {
const result = await axios(
'http://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将会执行一次,若是是一个空数组的话,hooks将不会在组件更新的时候执行,由于它没有监听到任何的变量。axios
这里还有一个陷阱,在代码中,咱们使用async/await从第三方的API中获取数据,根据文档,每个async函数都将返回一个promise,async函数声明定义了一个异步函数,它返回一个asyncFunction对象,异步函数是经过事件循环异步操做的函数,使用隐式Promise返回其结果。可是,effect hook应该不返回任何内容或清除功能,这就是为何你会在控制台看到如下警告: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..
这就是为何不容许在useEffect函数中直接使用async的缘由。让咱们经过在effect内部使用异步函数来实现它的解决方案。
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(
'http://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获取数据。可是,若是你对错误处理、加载提示、如何从表单中触发数据获取以及如何实现可重用的数据获取hook感兴趣,请继续阅读。
好的,咱们在mount后获取了一次数据,可是,若是使用input的字段来告诉API哪个话题是咱们感兴趣的呢?“Redux”能够做为咱们的默认查询,若是是关于“React”的呢?让咱们实现一个input元素,使某人可以获取“Redux”之外的话题。所以,为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(
'http://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; 复制代码
目前,这两个状态彼此独立,但如今但愿将它们耦合起来,以获取由input中的输入来查询指定的项目。经过下面的更改,组件应该在挂载以后经过查询词获取全部数据。
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;
复制代码
还差一部分:当你尝试在input中输入一些内容时,在mount以后就不会再获取任何数据了,这是由于咱们提供了空数组做为第二个参数,effect没有依赖任何变量,所以只会在mount的时候触发,可是如今的effect应该依赖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]);
return (
...
);
}
export default App;
复制代码
如今每当input的值更新的时候就能够从新获取数据了。但这又致使了另外一个问题:对于input中键入的每一个字符,都会触发该效果,并执行一个数据提取请求。如何提供一个按钮来触发请求,从而手动hook呢?
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依赖于于search,而不是随输入字段中变化的query。一旦用户点击按钮,新的search就会被设置,而且应该手动触发effect hook。
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;
复制代码
此外,search的初始值也设置为与query相同,由于组件也在mount时获取数据,所以结果应反映输入字段中的值。可是,具备相似的query和search状态有点使人困惑。为何不将实际的URL设置为状态而来代替search?
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [url, setUrl] = useState(
'http://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依赖于哪一个状态。一旦在点击或其余effect中设置此状态,此effect将再次运行。在这种状况下,若是URL状态发生变化,effect将再次运行以从API获取数据。
让咱们为数据获取引入一个加载提示。它只是另外一个由state hook管理的状态。loading被用于在组件中渲染一个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(
'http://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进行数据获取(当组件mount或URL状态更改时发生),加载状态将设置为true。一旦请求完成,加载状态将再次设置为false。
若是在React Hooks中加上错误处理呢,错误只是用state hook初始化的另外一个状态。一旦出现错误状态,应用程序组件就能够为用户提供反馈。使用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(
'http://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; 复制代码
到目前为止,咱们只有input和按钮的组合。一旦引入更多的输入元素,您可能须要用一个表单元素包装它们。此外,表单还能够经过键盘上的“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> ); } 复制代码
可是如今浏览器在单击提交按钮时页面会从新加载,由于这是浏览器在提交表单时的固有行为。为了防止默认行为,咱们能够经过event.preventDefault()取消默认行为。这也是在React类组件中实现的方法。
function App() {
...
const doFetch = () => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
};
return (
<Fragment> <form onSubmit={event => { doFetch(); 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> ); } 复制代码
如今,当你单击提交按钮时,浏览器不会再从新加载。它和之前同样工做,但此次使用的是表单,而不是简单的input和按钮组合。你也能够按键盘上的“回车”键。
为了提取用于数据获取的自定义hook,请将属于数据获取的全部内容,移动到一个本身的函数中。还要确保可以返回App组件所须要的所有变量。
const useHackerNewsApi = () => {
const [data, setData] = useState({ hits: [] });
const [url, setUrl] = useState(
'http://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]);
const doFetch = () => {
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`);
};
return { data, isLoading, isError, doFetch };
}
复制代码
如今,你能够在App组件中使用新的hook了。
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useHackerNewsApi();
return (
<Fragment> ... </Fragment>
);
}
复制代码
接下来,从dofetch函数外部传递URL状态:
const useHackerNewsApi = () => {
...
useEffect(
...
);
const doFetch = url => {
setUrl(url);
};
return { data, isLoading, isError, doFetch };
};
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> ); } 复制代码
初始状态也能够变为通用状态。把它简单地传递给新的自定义hook:
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]);
const doFetch = url => {
setUrl(url);
};
return { data, isLoading, isError, doFetch };
};
function App() {
const [query, setQuery] = useState('redux');
const { data, isLoading, isError, doFetch } = useDataApi(
'http://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; 复制代码
这就是使用自定义hook获取数据的方法。hook自己对API一无所知。它从外部接收全部参数,只管理必要的状态,如数据、加载和错误状态。它执行请求并将数据做为自定义数据获取hook返回给组件。
reducer hook返回一个状态对象和一个改变状态对象的函数。dispatch函数接收type和可选的payload。全部这些信息都在实际的reducer函数中使用,从之前的状态、包含可选payload和type的action中提取新的状态。让咱们看看这在代码中是如何工做的:
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 Hook接受reducer函数和一个初始化的状态对象做为参数,在咱们的例子中,数据、加载和错误状态的初始状态的参数没有改变,可是它们被聚合到由一个reducer hook管理的一个状态对象,而不是单个state hook。
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函数发送的对象包括一个必填的type属性和可选的payload。type告诉Reducer函数须要应用哪一个状态转换,而且Reducer还可使用payload来提取新状态。毕竟,咱们只有三种状态转换:初始化获取过程,通知成功的数据获取结果,以及通知错误的数据获取结果。
在自定义hook的最后,状态像之前同样返回,可是由于咱们有一个状态对象,而再也不是独立状态,因此须要用扩展运算符返回state。这样,调用useDataApi自定义hook的用户仍然能够访问data、isloading和isError:
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
...
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
复制代码
最后,还缺乏了reducer函数的实现。它须要处理三种不一样的状态转换,即FETCH_INIT、FETCH_SUCCESS和FETCH_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函数能够经过其参数访问当前状态和action。到目前为止,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();
}
};
复制代码
如今,每一个状态转换(由操做的type决定)都将基于先前的状态和可选的payload返回一个新的状态。例如,在成功请求的状况下,payload用于设置新状态对象的数据。
总之,reducer hook确保状态管理的这一部分是用本身的逻辑封装的。经过提供type和可选payload,你将始终已一个可预测的状态结束。此外,你将永远不会进入无效状态。例如,之前可能会意外地将isloading和isError状态设置为true。在这个案例的用户界面中应该显示什么?如今,reducer函数定义的每一个状态转换都会致使一个有效的状态对象。
即便组件已经卸载(例如,因为使用react路由器导航而离开),设置组件状态也是react中的一个常见问题。我之前在这里写过这个问题,它描述了如何防止在各类场景中为unmount的组件设置状态。让咱们看看如何防止在自定义hook中为数据获取设置状态:
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]);
const doFetch = url => {
setUrl(url);
};
return { ...state, doFetch };
};
复制代码
每一个effect hook都有一个clean功能,在组件卸载时运行。clean函数是从hook返回的一个函数。在咱们的例子中,咱们使用一个名为didCancel的布尔标志,让咱们的数据获取逻辑知道组件的状态(已装载/未装载)。若是组件已卸载,则标志应设置为“tree”,这将致使在最终异步解决数据提取后没法设置组件状态。
注意:事实上,数据获取不会停止——这能够经过axios的Cancellation实现——可是对于未安装的组件,状态转换会再也不执行。由于在我看来,axios的Cancellation并非最好的API,因此这个防止设置状态的布尔标志也能起到做用。
你已经了解了在React中state和effect hook如何用于获取数据。若是您对使用render props和高阶组件在类组件(和函数组件)中获取数据很感兴趣,请从一开始就去个人另外一篇文章。不然,我但愿本文对您了解react hook以及如何在现实场景中使用它们很是有用。