React中如何优雅的发送网络请求

2,038 阅读4分钟

分享是人类进步的阶梯

平常工作中经常要用到网络请求,今天就来聊聊如何优雅的发送网络请求

image.png

前置知识:熟悉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

effectreturn要么没有,要么是一个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: []}})

在某些情况下也不是不可以

image.png

但写法上不够优雅,比如我们要设置loading的状态,我们可能需要这样写

setResourceData(pre => ({...pre, loading: true}))

那么有没有一种更优雅的方式去解决该问题呢,答案是:有!

这里我们需要借助到useReduceruseStateuseReducer原理其实是一样的)

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~

image.png

我觉得还是不够优雅,可以再进一步。因为每次都写这些代码其实在浪费我们宝贵的时间,既然每次逻辑都一样,我们直接自定义一个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…

如果能看到这一步,给自己一点掌声

image.png

注意: useFetchData传入的参数要保证都是非必要不变化的,因为我们内部effect会依赖传入的参数,如果每次更新都会变化,就会陷入死循环了,比如传入的网络请求参数是一个对象时,如果直接这样写

useFetchData(fetchData, {a: xxx})

由于{a: xxx}每次更新都会创建一个新的对象传给useFetchDataeffect认为params变化了,又会发送网络请求,这就陷入死循环了,我们可以用useMemo做一层包裹

优化:

  1. useFetchData每次都会在组件挂载的时候就发送网络请求,有时候我们并不希望一开始就发送网络请求,所以我们可以加个条件来控制它什么时候发送网络请求
  2. 不仅是我们写的hook,所有在函数组件中写网络请求,在组件卸载的时候,依旧会进行状态的赋值
  3. 如果短时间多次发送网络请求,且前面的网络请求返回速度低于后面的返回速度,就会将状态设置为旧的状态,而不是最新我们想要的

第1点可以自己思考下如何实现,对于2,3点我分享一个简单的解决方法

useEffect(() => {
    let didCancel = false
    async fetchData() {
        const res = await fetch()
        !didCancel && setData(res.data)
    }
    fetchData()
    return () => {
        didCancel = true
    }
}, [])

Over~~~

image.png