React Hooks通关指南,不再做菜鸟

1,057 阅读9分钟

前言

目前团队中使用的都是ReactHooks写法,对于从来没接触过ReactHooks实战项目的小凌来说的确是不小的挑战。为了让更多新手能够快速入门ReactHooks,特此来写一篇ReactHooks的学习心路。

image.png

为什么要学ReactHooks?

Hooks的出现起初是为了让函数式组件(Function)也能支持状态的管理,但在学习过程中我认识到了很多Hooks的优点。

Function和Class的纠结: 函数式组件(Function)更高效。类组件(Class)有状态管理,方便后期拓展。学了Hooks之后我们不必在纠结

繁琐的生命周期: 在刚学React的时候我们总需要清楚的记住一个组件的生命周期和执行时机。在学了Hooks之后我们可以完全抛开生命周期了。

this指向问题: React的this指向问题总是令人头疼,网上虽然了解决方案,但是额外的代码总让人觉得麻烦。在学了Hooks之后,我们再也不用考虑这个问题了。

更简洁的代码: 相比传统方法而言,实现一个功能Hooks方法所用的代码量将更少。

image.png

与传统实现对比(Example)

原始方法实现一个计数器

import React, { Component } from 'react';
class Example extends Component {
constructor(props) {
super(props);
this.state = {count:0}
}
render() {
    return (
        <div>
            <div>点击我{this.state.count} 了次</div>
            <button onClick={()=>{this.addCount() }} >点击我</button>
            {/* <button onClick={this.addCount.bind(this)} >点击我</button> */}
        </div>
    );
}
// 计数器
    addCount(){
      this.setState({count: this.state.count + 1 })
    }
}
export default Example;

使用Hooks实现一个计数器

import React, { useState } from 'react'
function Example(){
// 声明一个值 [ 数值, 设置函数 ] 0为初始值 (声明不能存在于条件判断中)
const [ count, setCount ] = useState(0);
    return (
    <div>
        <div>useState点击我{count} 了次</div>
        <button onClick={()=>{setCount( count + 1 )}} >点击我</button>
    </div>
    )
}
export default Example;

相比于传统方法Hooks的实现直接减少了三分之一的代码量。在代码上更简洁,也更容易维护。

接下来我们来介绍一下ReactHooks的主要模块以及用法。

useState(状态管理)

useState使普通函数组件也有了状态管理的能力。其实我们在上方的代码里已经使用了useState,在这一模块里将详细介绍它的用法。我们从声明一个state开始:

image.png

左侧数组部分为ES6的解构赋值,在后续的讲解中我们将大量用到此类语法。

数组第1个参数为状态值(用于控制页面的展示和逻辑处理),第2个参数是更改值的方法。useState方法需要一个参数,这个参数就是状态值默认值

注意点

不要在条件语句中声明hooks

✖错误案例

const  [ name, setName ] = useState('小凌');
const isShowAge = false;
if(isShowAge){
  const [ age, setAge ] = useState(26);
}
const  [ hoppy, setHobby ] = useState('做菜');

控制台报错

image.png

Hooks渲染是从上到下依次执行,在if语句内使用的话,由于第一次未执行到useState,后面render时却又突然检测到了,就会导致控制台报错(可以理解成渲染时突然发现一个未知的useState,老版本不会报错,但是新版本将这个问题修复了所以控制台会报错)。

为什么我更改了对象state视图却没刷新?

当我们设置状态为数组或对象且只想改变其中一项属性时。

✖错误案例

const [ info, setInfo ] = useState({name:'小凌', age:26});
const changeAge = () => {
    let setInfo = info;
    setInfo.age = 18
    setInfo(setInfo)
}
return (
    <div>
        <div>姓名:{info.name} 年龄:{info.age}</div>
        <button onClick={changeAge} >设置年龄</button>
    </div>
)

发现视图并没有刷新,因为info的指针指向未变。

正确方法是使用解构赋值或是深浅拷贝的形式。

let setInfo = info;
setInfo.age = 18
setInfo({...setInfo})

useEffect(副作用)

useEffect主要用于监听状态值变化、在构建组件时进行监听、在销毁组件时进行监听。像极了类组件的

componentDidUpdatecomponentDidMountcomponentWillUnmount生命周期函数钩子。

之前我也会用类组件的生命周期来类比Hooks的执行时机。后来发现这并不是一个很好的方法。首先这个方法根本没有办法准确类比,其次React18中新增了API,OffScreen可以对Hooks的执行时机造成影响。

Function Component 仅描述 UI 状态,React 会将其同步到 DOM,仅此而已。

关于参数

useEffect方法有两个参数,第1个参数是要执行的函数,第2个参数是一个依赖项数组(根据具体需要监听什么状态值来决定数组内要填写什么)。

image.png

参数详解

1、不传递

useEffect不传递第二个参数会导致每次渲染都会运行useEffect。

useEffect(() => {
  console.log('使用useEffect')
  // 所有更新都执行
})

2、传递空数组

仅在挂载和卸载的时候执行,如果是多个就是其中某个更改时调用

useEffect(()=>{
    console.log('使用useEffect')
},[]) // 仅在挂载和卸载的时候执行

3、传递非空数组

在监听值更新时才会触发

useEffect(()=>{
    console.log(count)
},[count]) // count更新时执行

3、返回函数

在组件销毁时调用函数

const time = null;
useEffect(() => {
    const time = setInterval(() => {
        setCount(count + 1);
    }, 1000);
    return () => clearInterval(time); // 这个方法在组件销毁的时候会被调用
}, []);

useContext(跨组件通信)

useContext主要用于父子组件之间状态的跨级传递,实现了状态的共享(类似于Vue的Vuex)。

在认识useContext之前,与孙组件的状态传递是通过props

image.png

我们可以看到,当层级一多参数传递就变得复杂。使用useContext后,父组件产生的state将直接被孙组件消费

image.png

具体实现

父组件

export const CountContext = createContext()
function Example(){
    const [count,setCount] = useState(0)
    return (
        <div>
            <div>useContext点击我{count} 了次</div>
            <button onClick={()=>{setCount( count + 1 )}} >点击我</button>
            <CountContext.Provider value={count}>
                <Counter />
            </CountContext.Provider>
        </div>
    )
}

子孙组件

import { CountContext } from './父组件位置';
function Counter(){
// 使用父组件的count参数
    let count = useContext(CountContext)
    return (<h2>{count}</h2>)
}

注意点

新创建的DOM节点将在Provider之外。

当我们通过document.createElement创建Dom并将我们的内容挂载上去时,该节点将不会享有上下文的状态管理(也就是该节点在Provider之外)。

image.png

useMemo(状态缓存)

useMemo 是以缓存状态的形式来对渲染上进行性能优化的手段。

我们都知道只要状态更改了,那么组件视图就会从新渲染。

查看以下案例:

image.png

父组件代码

function App() {
    const [name, setName] = useState('名称')
    const [content,setContent] = useState('内容')
    return (
        <div>
            <button onClick={() => setName(new Date().getTime())}>name</button>
            <button onClick={() => setContent(new Date().getTime())}>content</button>
            <Button name={name}>{content}</Button>
        </div>
    )
}

子组件代码

function Button({ name, children }) {
    function changeName(name) {
        console.log('11')
        return name + '改变name的方法'
    }
    const otherName = changeName(name) 
    return (
        <>
        <div>{otherName}</div>
        <div>{children}</div>
        </>
    )
}

我们期望只有当name发生改变时候,才触发子组件的changeName方法。但当我们改变content时,也触发了changeName。因为父组件的重新渲染也重新渲染了子组件

使用useMemo对状态进行缓存,只有改变的时候才触发相应方法。具体代码如下

优化之后的子组件

function Button({ name, children }) {
    function changeName(name) {
        console.log('xiaoling')
        return name + '改变name的方法'
    }
    const otherName = useMemo(()=>changeName(name),[name])
    return (
        <div>
            <div>{otherName}</div>
            <div>{children}</div>
        </div>
    )
}

只在 React 函数中调用 Hook

import { CountContext } from './父组件位置';
// 公共部分提取出来
let count = useContext(CountContext)
function Counter(){
    // let count = useContext(CountContext)
    return (<h2>{count}</h2>)
}
function Counter2(){
    // let count = useContext(CountContext)
    return (<h2>{count}</h2>)
}

当我们文件中有两个组件都需要一个context时,简化代码的思想促使我们感觉需要将它提取出来。

image.png

发现了如上报错。这是万万不可取的。在官网中我们也可以找到相应的提示。必须在React的函数组件中使用Hooks

image.png

useCallback(方法缓存)

和之前学的useMemo一样,useCallback也是用来进行性能优化的,不同的是useCallback缓存的是方法。在小凌刚学Hooks的时候就写过几次死循环的代码。

大家先看如下代码:

function App() {
const [val, setVal] = useState("");
function getData() {
    setTimeout(()=>{
        setVal('new data '+count);
        count++;
    }, 500)
}
    useEffect(()=>{
        getData();
    }, []);
    
    return (
        <div>{val}</div>
    );
}

我们使用setTimeout来模拟对后端进行请求。

在这种场景下,没有useCallback什么事,组件本身是高内聚的。如果涉及到组件通讯,情况就不一样了:

function App() {
    const [val, setVal] = useState("");
    function getData() {
        setTimeout(() => {
        setVal("new data " + count);
    count++;
    }, 500);
}
    return <Child val={val} getData={getData} />;
}
function Child({val, getData}) {
    useEffect(() => {
        getData();
    }, [getData]);
    return <div>{val}</div>;
}
  1. App渲染Child,将valgetData传进去

  2. Child使用useEffect获取数据。因为对getData有依赖,于是将其加入依赖列表

  3. getData执行时,调用setVal,导致App重新渲染

  4. App重新渲染时生成新的getData方法,传给Child

  5. Child发现getData的引用变了,又会执行getData

  6. 3 -> 5 是一个死循环

在我们明确异步方法只执行一次的情况下,我们可以使用useCallback对其进行固定。

const getData = useCallback(() => {
setTimeout(() => {
    setVal("new data " + count);
count++;
}, 500);}, []);

数组内的参数和之前学的useeffect一样,是它的依赖项目。

useReducer

useReducer可以说是管理useState的集合。利用了Redux的理念,将多个state合并为了一个

image.png

案列

const initState = {
    name:0,
    age: 0,
}
function Example(){
    const [info,dispatch] = useReducer((state,action)=>{
        switch(action.type){
            case 'name':
                return {...state,name:action.value}
            case 'age':
                return {...state,age:action.value}
            default:
                return state
        }
    },initState)
    return (
        <div>
            <div>name:{info.name} age:{info.age}</div>
            <button onClick={()=>{dispatch({type:'name',value:Math.random()})}} >change name</button>
            <button onClick={()=>{dispatch({type:'age',value:Math.random()})}} >change age</button>
        </div>
    )

实现Redux

结合我们之前所学的useContext们边可以实现一个简单的Redux

案列如下

image.png

当我们点击AddColor组件时,为文字添加颜色。当我们点击AddBack组件时,为文字添加背景色

核心Provider


// 默认值

const initState = {

    color:'black',

    background:'#fff'

}

const reducer = (state,action)=>{

    switch(action.type){

        case UPDATE_COLOR:

            return  {...state,color:action.value};

        case UPDATE_BACK:

            return  {...state,background:action.value}

        default:

            return state

    }

}

export const Color = props =>{

    const [state,dispatch] = useReducer(reducer, initState)

    return (

        <ColorContext.Provider value={{state,dispatch}}>

            {props.children}

        </ColorContext.Provider>

    )

}

useRef

useRef主要用于建立中间值或是用来调用子组件方法

image.png

useRef返回一个可变的ref对象,其.current属性被初始化为传入的参数(initialValue)。返回的ref对象在组件的整个生命周期内不变

image.png

比如我们设定一个每次按一次+1的count属性,当count为奇数时隐藏。这个显示和隐藏不能单独作为一个state,这时候我们就可以用useRef去控制。


const isShow = useRef(false)

const changeCount = () => {

isShow.current = count + 1 % 2 === 1;

setCount(count + 1);

}

return (<button onClick={changeCount} >addCount</button>

{isShow.current ?<div>{showCount}</div> : null })

父组件调用子组件方法

父组件

const childRefRef = useRef();
return (
    <>
        <Child ref={childRef}/>
        <button onClick={() => childRefRef?.current?.childDo()}>调用子组件方法</button>
    </>
);

子组件

function Child(props, ref) {
    // 暴露子组件方法
    useImperativeHandle(ref, () => ({
        childDo,
    }));
    // 子组件方法
    const childDo = () => {
    };
return (
    <>
        老凌的文章太棒了我要点赞
    </>
);
}

export default forwardRef(Child);

如此我们就可以调用子组件方法了。

image.png

自定义Hooks

自定义Hooks在实现上也非常简单。你需要建立一个useXXX的函数。这个函数在形式上和普通函数没有区别,你

可以传递任意参数给这个Hooks。与普通函数的区别在于内部有没有实现其他Hooks。若内部没有实现其他Hooks,这个函数就不是自定义Hooks

如下例子,我们声明一个组件。

给调用方开启组件方法关闭组件方法组件内容的参数。

其中,开启关闭方法都是设置组件自己的state

image.png

声明自定义Hooks

const open = () =>{
    // 展示组件
    setState(true);
}
const close = () => {
    // 隐藏组件
    setState(false);
}
return {
    open,
    close,
    el: (
        <div></div>
    ),
};

使用自定义Hooks

const {
    open,
    close,
    el: openYourEl,
} = useYour();
return(
    <div>
        {openYourEl}
    </div>
)

至此,所有Hooks都过完啦。官网还提供了其他的Hooks

参考文章与项目地址

《useCallback》

本文章代码地址:rh-project