React: Hooks入门-手写一个 useAPI

2,337 阅读4分钟

react-hooks 入门

写在最前面

  • 最近项目 升级了react 16.8+,接入了 hooks,这里学习一下最基础的几个官方 hooks

  • 下面是官网文档的链接,基础知识掌握不牢靠的朋友可以再看看,官网的文档可以说是非常完整和浅出了。我的文章主要讨论具体的几个 hooks 的具体使用场景。

  • zh-hans.reactjs.org/docs/hooks-…

1、useEffect

  • 官方 demo
mport React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect 做了什么?

  • 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

2、useCallback 和 useMemo

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

  • 为了节约内存,我们可以把接口获取的数据先使用 useCallback 和 useMemo 做临时存储。这种优化有助于避免在每次渲染时都进行高开销的计算。
  • useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
  • how to use useCallback
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const getDownloadFile = useCallback(async () => {
    setLoading(true);

    try {
        const res = await axios.get(API.CUSTOMER.xxx(), {
            params: { customer_id: 123 }
        });

        setData(res.data as any);
    } catch (error) {
        setError(error);
    }

    setLoading(false);
}, []);
    
// 这里的 useEffect() 替代了以前的生命周期做的事情
useEffect(() => {
   getDownloadFile();
}, [getDownloadFile]);

3、useContext()

我们需要先创建一个 context 对象(React.createContext),接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。

了解了上面解构 hooks 下面我们来实战一下

## 这里我们来写一个简单的 useAPI

import { useState } from 'react';
import axios from 'axios';

// 首先定义一下类型
type UseApiResponse = [
    {
        loading: boolean,

        data: null | object | any[],

        error:  any
    },
    /**
     * 返回一个 promise 对象
     */
    (requestData?: any[] | object) => Promise<any>,
];

type UseApiArgs = {
    /**
     * HTTP Method. 默认 'GET'
     */
    method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS',

    url: string,

    /**
     * 可选:初始默认值
     */
    defaultData?: object | any[],

    /**
     *  返回数据
     */
    bodyData?: object | any[],

};

export const useApi = ({
    method = 'GET',
    url,
    defaultData,
}: UseApiArgs) => {
    const [loading, setLoading] = useState(false);
    const [data, setData] = useState(defaultData | null)
    const [error, setError] = useState(null);
   
    const sendRequest = (requestData?: object | any[]) => {
        const requestConfig = {
            method,
            url,
            data: requestData,
        };

        const axiosConfig = Object.assign({}, requestConfig);

        /**
         * 返回一个 promise 对象
         */
        return new Promise(async (resolve, reject) => {
            setLoading(true);
            try {
                const response = await axios(axiosConfig);
                setData(response.data);
                resolve(data);
            } catch (err) {
                setError(error);
                reject(err);
            } finally {
                setLoading(false);
            }
        });
    };

    const response: UseApiResponse = [
        {
            loading,
            data,
            error
        },
        sendRequest,
    ];
    return response;
};

export default useApi;

上面满足了基本的调用server api 的需求,但是远远是不能满足一些复杂的情况的,我们下面来升级一下我们扥 hooks,增加状态码,增加加载状态,主动触发 request 的需求等等

升级版

import { createContext, useState, useEffect, useContext } from 'react';
import axios from 'axios';

/**
 * ApiContext  这里可以配置全局的 config.
 */
export const ApiContext = createContext({});

// 首先定义一下类型
type UseApiResponse = [
    {
        loading: boolean,

        data: null | object | any[],

        error: Error || null,

        /**
         * The HTTP status number
         */
        status: number,

        /**
         * True unless and until the request has been triggered at least once
         */
        initialLoad: boolean,

        /**
         * pending 状态
         */
        pendingOrLoading: boolean,

        /**
         * axios 对象
         */
        responseObj: object,
    },
    /**
     * 返回一个 promise 对象
     */
    (requestData?: any[] | object) => Promise<any>,
];

type UseApiArgs = {
    /**
     * HTTP Method. 默认 'GET'
     */
    method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'OPTIONS',

    url: string,

    /**
     * 可选:初始默认值
     */
    defaultData?: object | any[],

    /**
     *  返回数据
     */
    bodyData?: object | any[],

    /**
     * 可选 : 如果你想主动调用 request, 设置为 true
     */
    autoTrigger?: boolean,
};

export const useApi = ({
    method = 'GET',
    autoTrigger = true,
    url,
    defaultData,
    bodyData,
}: UseApiArgs) => {
    const [initialLoad, setInitialLoad] = useState(true);
    const [loading, setLoading] = useState(false);
    const [data, setData] = useState(defaultData || null)
    const [error, setError] = useState(null);
    const [status, setStatus] = useState();
    const [responseObj, setResponseObj] = useState();

    /**
     * 你可以使用自定义的 api 来替代 Axios config
     */
    const globalConfig = useContext(ApiContext);

    const sendRequest = (requestData?: object | any[]) => {
        const requestConfig = {
            method,
            url,
            data: requestData,
        };

        const axiosConfig = Object.assign({}, globalConfig, requestConfig);

        /**
         * 返回一个 promise 对象
         */
        return new Promise(async (resolve, reject) => {
            setLoading(true);
            try {
                const response = await axios(axiosConfig);
                setResponseObj(response);
                setData(response.data);
                setStatus(response.status);
                resolve(data);
            } catch (err) {
                setError(error);
                reject(err);
            } finally {
                setLoading(false);
                if (initialLoad) setInitialLoad(false);
            }
        });
    };

    /**
     * 如果设置了自动触发这个参数,这里需要特殊处理一下,检查一下 initiaload 加载状态是否完成,然后处理 'POST', 'PATCH', 'PUT'
     */
    if (autoTrigger) {
        useEffect(() => {
            if (initialLoad) {
                /**
                 * Include body data if method allows
                 */
                if (['POST', 'PATCH', 'PUT'].includes(method)) {
                    sendRequest(bodyData);
                } else sendRequest();
            }
        }, []);
    }

    const response: UseApiResponse = [
        {
            loading,
            data,
            error,
            status,
            initialLoad,
            pendingOrLoading: initialLoad || loading,
            responseObj,
        },
        sendRequest,
    ];
    return response;
};

export default useApi;

怎么使用?

import React, { useEffect, useRef } from 'react';
import useApi, { ApiContext } from 'use-http-api';

const UserList = () => {
    const [{ loading, data }, getUsers] = useApi({
        url: 'https://reqres.in/api/users',
        defaultData: { data: [] },
    });

    return (
        <div>
            {loading ? (
                'Loading...'
            ) : (
                <ol>
                    {data.data.map(user => (
                        <li>{user.email}</li>
                    ))}
                </ol>
            )}
            <button onClick={() => getUsers()}>Update</button>
        </div>
    );
};

参考