分享是人类进步的阶梯
平常工作中经常要用到网络请求,今天就来聊聊如何优雅的发送网络请求
前置知识:熟悉useState, useEffect
先来个错误示范
useEffect(async () => {
const res = await axios("https://hn.algolia.com/api/v1/search?query=react");
setData(res.data);
}, []);
如果这样写的话,其实编辑器已经会报错了,因为async函数默认会返回一个Promise对象
Async functions always return a promise. If the return value of an async function is not explicitly a promise, it will be implicitly wrapped in a promise. —mdn
而effect的return要么没有,要么是一个clean up函数,因为effect的返回值如果有的话,会在更新或者卸载的时候进行函数调用
所以,我们可以把异步函数移到effect外面,effect再直接调用函数。或者把异步函数整体移到里面,再直接调用,这里就采用第二种方式吧,避免多一个hook去理解
useEffect(() => {
async function fetchData() {
const res = await axios("https://hn.algolia.com/api/v1/search?query=react");
setData(res.data);
}
fetchData()
}, []);
这样的话我们已经实现了在组件挂载的时候就发送网络请求,并将返回结果存到state中
下面再探究第二个问题,记录loading以及error状态,因为有时候我们需要根据这二者状态做一些不同的反馈
const [loaindg, setLoading] = useState(false);
const [error, setError] = useState(false);
const [data, setData] = useState({ hits: [] });
useEffect(() => {
async function fetchData() {
setLoading(true);
setError(false);
try {
const res = await axios("https://hn.algolia.com/api/v1/search?query=react");
setData(res.data);
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
这样我们就完成了loading以及error的状态记录了
你们有没有发现,我们一共用了三个状态分别记录loading, error, data。这样其实显得有点繁琐,这些状态其实是互相关联的,所以我们可以把它放到一起,可能有的人会问,那改成这样可以吗
const [sourceData, setSourceData] = useState({loading: false, error: false, data: {hits: []}})
在某些情况下也不是不可以
但写法上不够优雅,比如我们要设置loading的状态,我们可能需要这样写
setResourceData(pre => ({...pre, loading: true}))
那么有没有一种更优雅的方式去解决该问题呢,答案是:有!
这里我们需要借助到useReducer(useState与useReducer原理其实是一样的)
const reducer = (state, action) => {
switch (action.type) {
case "FETCH_INIT":
return {...state, loading: true, error: false}
case "FETCH_SUCCESS":
return {...state, data: action.payload}
case "FETCH_FAILURE":
return {...state, error: true}
case "FETCH_END":
return {...state, loading: false}
default:
return state
}
}
export default function App() {
const [state, dispatch] = useReducer(reducer, {loading: false, error: false, data: {hits: []}})
useEffect(() => {
async function fetchData() {
dispatch({type: "FETCH_INIT"})
try {
const res = await axios("https://hn.algolia.com/api/v1/search?query=react");
dispatch({type: "FETCH_SUCCESS", payload: res.data})
} catch (error) {
dispatch({type: "FETCH_FAILURE"})
} finally {
dispatch({type: "FETCH_END"})
}
}
fetchData();
}, []);
我们只需要关注每次发送怎样的action,具体action的处理由reducer内部来做,与我们无关,这样逻辑就清晰简单
你以为到这里我们就结束了吗? NoNoNo~
我觉得还是不够优雅,可以再进一步。因为每次都写这些代码其实在浪费我们宝贵的时间,既然每次逻辑都一样,我们直接自定义一个useFetchData不就好了吗,只要给这个useFetchData传:发送网络请求的函数、以及所需的参数(如果有)
最后成了这个样子
/* useFetchData.js */
const useFetchData = (fetch, params) => {
const [state, dispatch] = useReducer(reducer, {
loading: false,
error: false,
data: { hits: [] }
});
useEffect(() => {
async function fetchData() {
dispatch({ type: "FETCH_INIT" });
try {
const res = await fetch(params);
dispatch({ type: "FETCH_SUCCESS", payload: res.data });
} catch (error) {
dispatch({ type: "FETCH_FAILURE" });
} finally {
dispatch({ type: "FETCH_END" });
}
}
fetchData();
}, [fetch, params]);
return { ...state };
};
/* api.js */
const fetchArtical = (param) => {
return axios(`https://hn.algolia.com/api/v1/search?query=${param}`);
};
/* App.js */
import { fetchArtical } from "./api";
import { useFetchData } from "./hooks";
export default function App() {
const [params, setParams] = useState("react");
const { loading, error, data } = useFetchData(fetchArtical, params);
return (
<ul>
{data.hits.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
);
}
到这一步,差不多算优雅了,以后每次发送网络请求,就直接使用useFetchData,传入网络请求函数,以及所需参数即可,之前的十几行代码浓缩到一行代码,省去不必要的重复编写
完整线上代码:codesandbox.io/s/fetch-qmx…
如果能看到这一步,给自己一点掌声
注意:
useFetchData传入的参数要保证都是非必要不变化的,因为我们内部effect会依赖传入的参数,如果每次更新都会变化,就会陷入死循环了,比如传入的网络请求参数是一个对象时,如果直接这样写
useFetchData(fetchData, {a: xxx})
由于{a: xxx}每次更新都会创建一个新的对象传给useFetchData,effect认为params变化了,又会发送网络请求,这就陷入死循环了,我们可以用useMemo做一层包裹
优化:
useFetchData每次都会在组件挂载的时候就发送网络请求,有时候我们并不希望一开始就发送网络请求,所以我们可以加个条件来控制它什么时候发送网络请求- 不仅是我们写的
hook,所有在函数组件中写网络请求,在组件卸载的时候,依旧会进行状态的赋值 - 如果短时间多次发送网络请求,且前面的网络请求返回速度低于后面的返回速度,就会将状态设置为旧的状态,而不是最新我们想要的
第1点可以自己思考下如何实现,对于2,3点我分享一个简单的解决方法
useEffect(() => {
let didCancel = false
async fetchData() {
const res = await fetch()
!didCancel && setData(res.data)
}
fetchData()
return () => {
didCancel = true
}
}, [])
Over~~~