在本教程中,我想告诉你如何在React中使用state hook和effect hooks去获取异步数据,这也是我们日常业务需求里最常用到的场景,全文例子会由浅入深逐步进行,最终带你实现一个自定义的hook用于数据获取,可以根据个人情况通过目录跳转到感兴趣的地方。
本文适合了解过React hook这一新特性的开发者,如果你还没接触过,可以先阅读 官方文档 。本文来源于官方博客加上个人使用理解的理解,文章里有可以在线调试体验的在线链接可供查看体验~
使用React Hook进行数据获取
如果你对React的数据获取不是很熟悉,可以去详读这篇文章,它会引导你使用React 的class组件进行数据获取,如何使用render prop组件或者hoc组件创建可以复用的组件,还有如何解决处理中和等待中的状态图标,我想在函数式组件中使用React Hooks想你展示上述的功能(指class组件获取数据)
import React, { useState } from 'react';
// 这是一个展示list列表的组件,它使用useState这个hooks提供的功能去维护和更新本地的state状态,这个钩子函数可以接收一个默认值为[]数组,该组件现在还没有使用setData设置任何状态值;
function App() {
// hits = Hacker News articles
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;错误示例🔗
下面代码里有2处错误,可以先自行发现哈
import React, { useState, useEffect } from 'react';
// npm install axios
//使用axios去获取数据,你也可以使用fetch之类的api
import axios from 'axios';
function App() {
const [data, setData] = useState({ hits: [] });
//使用useEffect配合axios去获取api的数据并且将返回值设置为 state hook的值,使用async/await去写异步代码
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会导致组件陷入死循环,因为我们在hook里设置了setData修改状态,每当状态更新时都会触发函数组件执行这个effect hooks,不停的去fetch数据,我们需要解决这个bug,因为我们只希望在组件完成挂载的时候执行hooks里的aioxs获取数据,可以通过传递useEffect的第二个参数为[]来让这个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的第二个参数可以提供这个hook更新所依赖的变量数组,当变量发生改变时hook才会再次执行,如果这个变量为空,hook将不会在组件更新的时候再次执行,因为这个hook没有监听任何变量;
上面的代码里还有一个新手经常遇到的陷阱(idel会报错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),我们知道async函数会隐式的返回一个Promise对象,而useEffect希望他的第一个参数什么都不返回或者返回一个清除函数(会在组件状态更新渲染后执行),因此useEffect函数中不允许直接使用async函数包装第一个参数,我们可以像下面这样在useEffect中使用async/await
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();
//匿名方法可以使用自执行的方式 async()();
}, []);
return (
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
export default App;
接下来讲一下如何在hooks里处理错误状态,loading状态,如何使用表单配合表单去获取数据,还有如何实现一个可以复用的获取数据的hook
手动触发hook 代码示例🔗
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=${query}`,
);
setData(result.data);
};
fetchData();
}, [query]);
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组件去告诉 API 我们感兴趣的文章是什么,初始内容是‘Redux’,当我们想查询其他文章,例如'react'时,我们需要告诉函数组件,于是新增加了一个state状态,query;我们希望组件挂载完成后会按照query的值来获取文章列表的数据,因此这个effect hook依赖于query,不再是[],在组件重新渲染时将监听query变量是否变化;
但是我们可能希望调用API的操作是可控的,不想要每次往input输入时都会发起API请求(必要场景可以使用防抖函数),因此可以再增加一个state:search,在我们点击查询按钮再调用API请求
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(
`https://hn.algolia.com/api/v1/search?query=${search}`,
);
setData(result.data);
};
fetchData();
//此时我们的hook依赖于search变量的值
}, [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>
);
}
export default App;这样看起来有点奇怪,我们会维护两个值完全相同的状态,如果依赖项更多表单状态值的话页面state就会变得混乱,我们可以把API的地址提取出来作为hook依赖的变量,点击查询按钮时如果发现查询参数改变,url发生变化才会执行useEffect这个hook,这样看起来似乎更合理一些了~
function App() {
const [data, setData] = useState({ hits: [] });
const [query, setQuery] = useState('redux');
const [page, setPage] = useState(1);
const [url, setUrl] = useState(
'https://hn.algolia.com/api/v1/search?query=redux&page=1',
);
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)}
/>
<input
type="text"
value={page}
onChange={event => setPage(event.target.value)}
/>
<button
type="button"
onClick={() =>
setUrl(`https://hn.algolia.com/api/v1/search?query=${query}&page=${page}`)
}
>
Search
</button>
<ul>
{data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
</Fragment>
);
}loading状态
数据获取往往时异步的,为了更好的用户体验,我们通常会给在请求状态中增加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(
'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(`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>
)}
</Fragment>
);
}
export default 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);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(url);
//throw new Error('error') 为了能显示错误状态的效果可以手动抛一个错误
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;使用From表单
上面的例子里,我们使用了input和button的组合去让组件显示想要的数据,现在我们把这些表单组件放到Form组件里,注意,和之前不同的是,使用Form我们将会支持使用键盘上的 enter 来进行submit的操作。注意我们也需要防止点击按钮导致页面刷新的问题。
function App() {
...
return (
<Fragment>
<form
onSubmit={() =>
setUrl(`http://hn.algolia.com/api/v1/search?query=${query}`)
// 和类组件一样,为了组织Form表单的默认行为导致页面刷新,需要添加下面这行
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>
);
}自定义获取数据的hook 自定义HOOK🔗
为了提取一个自定义的hook用于数据获取,需要把所有用于数据获取的内容(包括loading状态和错误处理状态,除了由input控制的query字段)移到我们自定义的函数中,并且自定义的hook需要返回app组件必要的变量
//useHackerNesAPi.js
import React, { Fragment, useState, useEffect } from 'react';
import axios from 'axios';
const useHackerNesAPi=()=>{
const [data,setDasta]=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=asnyc ()=>{
setIsError(false);
setIsLoading(true);
try{
const result=await axios(url);
setData(result.data);
}catch(error){
setIsError(true);
}
setIsLoading(false);
}
},[url]);
return [{data,isLoading,isError},setUrl];
}
//app.js
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>
);
}如果我们在app组件里给hook里的url state传递初始值或者要查询的API地址状态,那么我们就可以获得一个更通用的自定义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]);
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;当当当,这就是我们要写出的自定义获取数据的hook,这个hook对要查询的API一无所知,它所有的参数均来自外部,并且只需要管理类似data,loading,error这样的必要状态,它执行API请求并将得到的数据返回给使用这个自定义hook的组件;
使用Reducer hook 代码示例🔗
进行到这一步,我们已经多种state hook来管理数据我们sh数据获取,loading和error的状态,但是,如你所见,所有这些状态都用于获取数据的函数,他们由各自的state hook管理却因为共同关注的问题(data fetch)而紧密联系成一体(这些state,如setIsError、setIsLoading被一个接一个的使用连成一体),现在让我们使用useReduce将他们组合起来
当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,你可能需要用useReducer去替换它们,为何要在这个例子里使用useReducer可以看看这篇文章:什么情况下使用useReducer/useState
一个Reduce hook向我们返回一个state对象和一个用于操作state dispatch函数(dispatch包含了action和payload参数),dispatch的action和payload参数用于hook里的reducer函数,通过之前的state获取一个全新的state
import { useState, useEffect, useReducer } from "react";
import axios from "axios";
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();
}
};
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
//useReducer第一个参数是一个reudce函数,第二个参数是初始状态
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]);
return [state, setUrl];
};
export default useDataApi;通过上面的思路我们可以提取一个自定义的hook组件,依然会返回data,loading,error状态,但是通过reducer,我们返回的是一个 state Object包含这三个状态;
通过swtich语句,我们可以根据action和state参数的不同分别返回state Object,
ok。现在我们例子中所有的state的变化都取决于action 的type,根据之前的state和payload参数返回一个新的state,例如,当请求成功时,payload用于设置全新的state object;
总之,Reducer Hook让我们用自己的逻辑代码压缩了部分状态管理hook,通过提供action type和可选的payload参数,你将始终可以得到一个符合预期的的state修改,另外可以避免当你意外的将类似isLoading和isError这样的state设置错误时页面UI如何显示的问题,但是现在因为reducer函数,每一种UI状态都可以指向一个特定的状态下的state object的。
竞态问题
这是一个React组件中的常见问题,如何避免异步请求的不可预测导致页面错误的UI更新,例如上面例子中连续查询2个你感兴趣的词汇,但是第一个请求的调用返回在第二次调用返回之后,下面是一个在hook组件中处理竞态问题的例子,当然这实际上并没有取消api的调用终止数据获取的行为(基于XHR对象可以使用abort api来阻止数据获取,例如axios),但是因为dispatch没有执行,页面的ui渲染就被阻止了。
const useDataApi = (initialUrl, initialData) => {
const [url, setUrl] = useState(initialUrl);
const [state, dispatch] = useReducer(dataFetchReducer, {
isLoading: false,
isError: false,
data: initialData,
});
useEffect(() => {
//添加一个flag来处理竞态问题
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();
//如果组件没有挂载完成,在状态改变重新渲染时会先执行上次hook的callback方法,上一次组件的加载过程就像是: fetch data 返回前设置didCancel为true,这样就无需执行dipatch函数更新页面UI了
return () => {
didCancel = true;
};
}, [url]);
return [state, setUrl];
};