React中这几个好用的Hook你还不会吗?快来学习一下

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 3 天,点击查看活动详情

大家好,我是爱吃鱼的桶哥Z。自从React V16.8版本增加了hooks后,我们不必在编写类型复杂的class组件即可操作组件的状态了,虽然hooks已经发布快三年了,但是现在依旧还有很多hooks值得我们继续深入学习和使用,今天就给大家带来几个我们常用的hooks。

状态管理

在我们日常的开发中,如果我们需要在多个组件中共用某些数据或状态,而这些页面的层级又没有关联性时,一般我们都需要借助第三方的库来实现,例如:ReduxMobxRecoil等,但是我们有时候往往又不想为了几个页面而引入一个第三方的状态库,这时候我们的主角就可以登场了,它就是createContext,其实这个API并不是React在16.8以后的版本添加的,在之前的版本中就已经有了,下面我们一起来学习一下这个API的具体使用方法。

React.createContext 这个 API 从名字就可以看出,createContext能够创建一个React的上下文(context),然后在订阅了这个上下文的组件中,可以拿到上下文中提供的数据或者其他信息。具体的API文档在这里,它的基本使用方法如下:

// context.tsx
const defaultContext = null;
export const Context = React.createContext(defaultContext);

其中的defaultContext是传入的默认值或初始值,如果我们在组件中需要使用创建的上下文,即在组件中使用公用的数据或状态,则需要在最外层组件上通过 Context.Provider 将组件包裹一层,并通过在组件上显示的添加 value={{xx: xx}} 的形式传入 value,用于指定context要对子组件暴露的相关信息。

为什么需要设定默认值呢?

因为子组件在使用的过程中,如果匹配不到最新的 Provider ,则会使用默认值。默认值一般在设定某些固定值或用于测试组件数据的时候会比较有用,一般我们都不会给一个固定的默认值用于实际的开发。

通过 React.createContext 我们创建了一个上下文的状态管理,那么我们该如何在相关的子组件中来使用这些公共的数据或状态呢?

useContext

在上面的内容中,我们使用React.createContext创建了React相关的上下文,在子组件中,我们需要通过 useContext 这个hooks来获取到Provider传递的相关内容,useContext 的文档可以看这里,它的基本使用方法如下:

import { context } from './context';

const { status } = useContext(Context);

通过上述的代码,我们可以发现useContext的参数是context这个Context实例,而不是字符串。之所以我们在上面要将context导出,然后在这个文件中导入来使用,就是为了避免父子组件不在一个文件夹中而无法共用context的情况。

我们的准备工作已经完成了,接下来我们使用 createContextuseContext 实现数据和方法的共享。举一个实际开发中的例子:在子组件中修改父组件的state。我们一般的做法是将父组件中的方法通过props传递给子组件使用,如果子组件的层级较深,则需要一层一层的传递,并且如果某个子组件不需要使用到这个方法,它也需要往下进行透传,使用起来及其麻烦,而我们使用Context的方式就能避免这样的问题发生。

首先我们需要在父组件中通过Provider包裹一层,并且通过Provider上的value将方法提供给子组件,具体的代码如下所示:

//Parent.tsx
import React, { useState } from 'react';
import { Context } from './context';
import Child from './components/Child';

// 模拟异步请求
const requestData = () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(true);
        }, 1000);
    });
}

const ParentNode: React.FC = () => {
    const [num, setNum] = useState<number>(0);
    const [count, setCount] = useState<number>(0);
    
    // 将父组件中的状态、方法通过Provider上的value方法传递下去
    return (
        <Context.Provider value={{setNum, setCount, requestData}}>
            <Child num={num} count={count} />
        </Context.Provider>
    )
};

export default ParentNode;

接下来子组件通过useContext来解析上下文,我们一起来看一下在子组件中该如何才能获取到父组件传递过来的方法。具体的代码如下:

//Child.tsx
import React, { useContext } from 'react';
import { Context } from './context';

interface ChildProps {
    num: number;
    count: number;
}

const Child: React.FC<ChildProps> = ({ num, count }) => {
    const { setNum, setCount, requestData } = useContext(Context);
    
    useEffect(() => {
        requestData().then((res) => {
            console.log(`Response Data: ${res}`);
        });
    }, []);
    
    return (
        <div>
            <p>num is: {num}</p>
            <p>count is: {count}</p>
            <hr />
            <div>
                <button onClick={() => setNum(num + 1)}>num++</button>
                <button onClick={() => setCount(count + 1)}>count++</button>
            </div>
        </div>
    )
};

export default Child;

具体的执行效果,可以狠戳这里

上述的例子中,当我们在Child组件中点击按钮时,直接调用了Parent中通过Context透传过来的方法,并且修改了父组件的状态,子组件自身则会重新渲染。这种方式显示的避免了props需要层层传递的问题。虽然我们这里的子组件与父组件都在同一目录且只有一级子组件,但即使存在多级子组件的情况,也是可以直接修改父组件传过来的数据的。

useReducer

上面的例子中我们虽然实现了多级组件方法共用,但是却暴露出一个问题:所有的方法都需要显示的放在Context.Provider.value中进行传递,这就会造成整个Context.Provider上面的方法会越来越多,也显的越来越臃肿,针对这个问题,我们再次引入一个新的hook,它就是useReduer,API文档地址在这里。如果看过一点React源码的童鞋,就会知道useState内部其实也是基于useReducer来进行数据的更新的,下面我们一起来看一下该如何改造上面的代码以解决我们提出的这个问题。

useReduer 基础使用方法如下:

const [state, dispatch] = useReducer(reducer, initialState);

其中reducer是我们需要执行的规则,如果之前用过Redux的童鞋,看到这里应该不会觉得陌生。如果这个地方看的不是很明白,可以自己动手写一下就大致清楚了。initialState是初始化的数据,也就是一开始我们获取或指定的默认值。下面的父组件与之前相比只是去掉了setNumsetCount等设置state的方法,并在Providervalue中只传入了dispatch,具体可以看一下相关的代码修改。

//Parent.tsx
import React, { useReducer } from 'react';
import { Context } from './context';
import Child from './components/Child';

export enum Action {
    NUM = "NUM",
    COUNT = "COUNT"
}

export interface CountAction {
    type: Action;
    payload: number;
}

interface CountState {
    count: number;
    num: number;
}

const initialState = { num: 0, count: 0 };

const reducer = (state: CountState, action: CountAction) => {
    const { type, payload } = action;
    switch(type) {
        case Action.NUM:
            return {
                ...state,
                num: state.num + payload
            };
        case Action.COUNT:
            return {
                ...state,
                count: state.count + payload
            };
        default:
            return state;
    }
};

const ParentNode: React.FC = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    
    return (
        <Context.Provider value={{ dispatch }}>
            <Child num={state.num} count={state.count} />
        </Context.Provider>
    )
};

export default ParentNode;

上述父组件传递了dispatch给子组件,因此子组件中只需要获取到dispatch即可修改父组件中的state,而子组件与之前相比,唯一修改的只是点击事件中不是执行setNumsetCount,而是执行dispatch,具体代码如下:

import React, { useContext } from "react";
import { Context } from "../../context";
import { Action } from "../Parent";

interface ChildProps {
    num: number;
    count: number;
}

const Child: React.FC<ChildProps> = ({ num, count }) => {
    const { dispatch } = useContext(Context);
    
    return (
        <div>
            <p>num is: {num}</p>
            <p>count is: {count}</p>
            <hr />
            <div>
                <button onClick={() => dispatch({ type: Action.NUM, payload: 1 })}>num++</button>
                <button onClick={() => dispatch({ type: Action.COUNT, payload: 5 })}>count++</button>
            </div>
        </div>
    )
};

export default Child;

具体的执行效果,可以狠戳这里

上述的代码修改中,我们解决了前面的问题,即在Context.Provider中暴露方法过多的问题,但是我们子组件中的state还是通过props进行传递的,因此这里我们一样可以将state也通过Context传递给子组件,这样我们的子组件和父组件之间就不需要通过props来取值了,大致的代码变化如下:

//Parent.tsx
...other code

const ParentNode: React.FC = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    
    return (
        <Context.Provider value={{ state, dispatch }}>
            <Child />
        </Context.Provider>
    )
};

export default ParentNode;

子组件也需要跟着相应的改变:

//Child.tsx

...other code

const Child: React.FC = () => {
    const { state, dispatch } = useContext(Context);
    const { num, count } = state;
    return (
        <div>
            <p>num is: {num}</p>
            <p>count is: {count}</p>
            <hr />
            <div>
                <button onClick={() => dispatch({ type: Action.NUM, payload: 1 })}>num++</button>
                <button onClick={() => dispatch({ type: Action.COUNT, payload: 5 })}>count++</button>
            </div>
        </div>
    )
};

export default Child;

最终修改的效果可以狠戳这里

经过上述几个步骤的修改,我们的状态和操作方法都已经可以在所有的子组件中进行共用了,但是现在又产生了一个新的问题,即使有些子组件没有用到某个状态,但是经过Context的透传,还是会引起子组件的渲染,这时候我们就需要告诉React哪些组件在自身的值没有发生变化时不需要重复渲染。

一般我们能用到的方法有:memouseMemo,但是当使用memo的时候,子组件还是会触发重复渲染。这是因为memo只会对props进行浅比较,而通过Context注入到子组件中的state并不是通过props来传递的,因此state的变化必然会触发组件的重渲染,为此我们就需要用到useMemo了。

其实在平时的面试中,如果问到关于React相关的题目,一般也会问memouseMemo的具体区别,那么你知道它们的区别吗?

useMemo

useMemo 官方的解释如下:把“创建”函数和依赖项数组作为参数传入useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

使用方法如下:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

下面是对子组件进行修改,在组件内部我们使用useMemo将组件进行一次包裹,并且声明所有的依赖性,这样只有当依赖性中的值发生改变,页面才会重新渲染,大致代码如下:

//Child.tsx

...other code

const Child: React.FC = () => {
    const { state, dispatch } = useContext(Context);
    return useMemo(() => {
        const { num, count } = state;
        return (
            <div>
                <p>num is: {num}</p>
                <p>count is: {count}</p>
                <hr />
                <div>
                    <button onClick={() => dispatch({ type: Action.NUM, payload: 1 })}>num++</button>
                    <button onClick={() => dispatch({ type: Action.COUNT, payload: 5 })}>count++</button>
                </div>
            </div>
        )
    }, [state, dispatch]);
};

export default Child;

优化后的代码可以狠戳这里

结尾

到这里,我们在这篇文章中使用到的hooks就全部都介绍完毕了,虽然上面的demo写的比较简单,但在实际开发中还是可以很方便的帮助我们来处理数据、方法的共享,我们一起再来回顾一下用到的hooks有哪些。

首先通过createContext来创建一个Context上下文,这个API不是16.8版本以后才有的,在之前的版本中就已经存在;其次通过useContext获取到Context传递到组件中的statesetNum,方便我们对数据进行操作;然后通过useReducer统一管理,方便在子组件中不用通过props来获取数据和方法;最后通过useMemo来优化组件,避免组件中state没有发生变化时组件重复渲染的问题。

至此,我们的全部内容就结束了,当然对于React的学习和使用我们还有很长的路要走,我们一起加油吧~

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

参考文献

Using the useReducer Hook in React with TypeScript

猜你喜欢

转载自juejin.im/post/7125602389016444965