React Hooks详解

1,828 阅读5分钟

类组件

在学习Hooks之前我们县看看传统的类组件是如何实现一个button组件的

    import React from 'react';

    class Button extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                btnText: '点击我'
            }
            this.handleClick = this.handleClick.bind(this);
        }

        render() {
            const { btnText = '按钮' } = this.state;
            return(
                <button
                    onClick={this.handleClick}>
                    {btnText}
                </button>
            )
        }

        handleClick() {
            this.setState({btnText:'你点击了我'})
        }
    }
    export default Button;

这个组件仅仅实现了一个可点击的button,可以看到代码已经很复杂了。基于此问题。在介绍Hooks之需要知道什么是函数组件。

函数组件

React 早就支持函数组件,下面就是一个例子

    function TopTips(props) {
        const { tips } = props;
        return(
            <div>{tips}</div>
        )
    }

但是这种组件有很大的局限,须是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类。为了解决函数组件能使用状态,于是React团队推出了Hooks.

Hooks概述

Hook 这个单词的意思是"钩子"。

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。主要包括以下几个内容:

  • 状态(useState)
  • 副作用(useEffect)
  • 上下文(useContext)
  • Redux(useReducer)
  • 记忆(useMemo)

useState():状态钩子

useState()用于为函数组件引入状态(state)。

前面的类组件,用户点击按钮,按钮文字改变,我们使用Hooks的useState()来实现如下

    import React, { useState } from 'react';

    export default function HooksButton (props) {
        const [btnText, setBtnText] = useState('点击我');
        function handleClick() {
            setBtnText('你点击了我')
        }
        return(
            <button
                onClick={handleClick}>
                {btnText}
            </button>
        )
    }

上面的代码中,HooksButton是一个函数组件,使用了useState()钩子引入状态。

useState()这个函数接受状态的初始值,作为参数,上例的初始值为按钮的文字。该函数返回一个数组,数组的第一个成员是一个变量(上例是btnText),指向状态的当前值。第二个成员是一个函数,用来更新状态,约定是set前缀加上状态的变量名(上例是setBtnText)。

useEffect:副作用钩子

useEffect用来引入具有副作的操作,最常见的是向服务请求数据。相当于类组件中的componentDidMount钩子函数。useEffect用法如下:

    useEffect(() => {
        // 异步请求
    }, [ dependencies ])

useEffect()接受两个参数,第一个是一个函数,主要是用来做一些异步操作;第二个是一个数组,用于给书effect依赖项;只要这个参数发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()

下面们看一个例子:

    import React, { useState, useEffect } from 'react';
    import Loading from './Loading';

    export default function OrderDetail(orderId){

        const [loading, setLoading] = useState(true);
        const [orderDetail, setOrderDetail] = useState({});

        useEffect(() => {
            setLoading(true);
            fetch(`https://xxx/${orderId}`)
            .then(res => res.json())
            .then(data => {
                setOrderDetail(data);
                setLoading(false);
            })
        }, [orderId])

        if (loading) {
            return (
                <Loading  text="加载中..."/>
            )
        }
        if (orderDetail?.orderDetail.title) {
            return (
                <div>
                    <div>{orderDetail.title}</div>
                    ....
                </div>
            )
        }
    } 

上面的代码中上面代码中,每当组件参数orderId发生变化,useEffect()就会执行。组件第一次渲染时,useEffect()也会执行。

useContext 共享状态钩子

如果需要在组件之间共享状态,可以使用useContext()

现在有两个组件 Navbar 和 Messages,我们希望它们之间共享状态。

    <div className="App">
        <Navbar/>
        <Messages/>
    </div>

第一步就是使用 React Context API,在组件外部建立一个 Context。

    const AppContext = React.createContext({});

组件封装代码如下:

    export default function HooksContext(props) {
        const AppContext = React.createContext({});
        return (
            <AppContext.Provider value={{username: 'useContext'}}>
                <div className="container">
                    <Navbar/>
                    <Messages/>
                </div>
            </AppContext.Provider>
        )
    }

上面代码中,AppContext.Provider提供了一个 Context 对象,这个对象可以被子组件共享。

Navbar 代码如下:

    const Navbar = () => {
    const { username } = useContext(AppContext);
    return (
        <div className="navbar">
            <p>{username}</p>
        </div>
    );
    }

上面代码中,useContext()钩子函数用来引入 Context 对象,从中获取username属性。

Messages 代码如下:

    const Messages = () => {
        const { username } = useContext(AppContext)
        return(
            <div className="messages">
                <p>{username}</p>
            </div>
        )
    }

useReducer

useReducer是React提供的一个高级Hook,类似于redux。它不像useEffect、useState、useRef等必须hook一样,没有它我们也可以正常完成需求的开发,但useReducer可以使我们的代码具有更好的可读性、可维护性、可预测性。

    const [count, dispath] =  useReducer((state,action) => {
        // 处理逻辑
    }, 0);

从上面的代码中可以看出,useReducer接受两个参数。第一个参数是一个reducer 函数,reducer 接受两个参数一个是 state另一个是 action。第二个参数是要更新的状态count的初始值。然后返回一个状态 countdispath,count 是返回状态中的值,而 dispatch 是一个可以发布事件来更新 state 的。

通过一段代码看看useReducer是如何使用的

    import React, { useReducer } from 'react';

    export default function HooksReducer(props){
        const [count, dispath] = useReducer((state, action) => {
            switch(action) {
                case 'add':
                    return state + 1;
                case 'sub': 
                    return state - 1;
                default:
                    return state;
            }
        }, 0)
        
        return (
            <div>
                <h1>{count}</h1>
                <button
                    onClick={()=> dispath('add')}
                    >+</button>
                <button
                    onClick={()=> dispath('sub')}
                    >-</button>
            </div>
        )
    }

上面的代码使用useReducer实现了一个简单的加减组件。

useMemo

根据React中文网给出的概念是:

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

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

也就是说传入useMemo中的函数只有在依赖项改变之后才会执行。下面我们来看个例子:

    function MemoExample() {
        const [height, setHeight] = useState(0);
        const [width, setWidth] = useState(0);
        const [area, setArea] = useState(0);
        function getSquareArea() {
            return area;
        }
        return(
            <div>
                <input 
                    value={height} onChange={event => setHeight(event.target.value)}/>
                <input 
                    value={width} onChange={event => setWidth(event. target.value)}/>
                <span>{getSquareArea()}</span>
                <button 
                    onClick={() => setArea(height * width)}>计算面积</button>
            </div>
        )
    }

上面这个组件,我们想在输入完长和宽之后点击计算面积按钮的时候在调用getSquareArea()方法,但是无论heightwidth变化的时候都会重新渲染组件,所以都会调用getSquareArea()方法。这种情况我们就可以使用useMemo

    function MemoExample() {
        const [height, setHeight] = useState(0);
        const [width, setWidth] = useState(0);
        const [area, setArea] = useState(0);
        const getSquareArea = useMemo(() => {
            return area;
        }, area);       
        return(
            <div>
                <input 
                    value={height} onChange={event => setHeight(event.target.value)}/>
                <input 
                    value={width} onChange={event => setWidth(event. target.value)}/>
                <span>{getSquareArea()}</span>
                <button 
                    onClick={() => setArea(height * width)}>计算面积</button>
            </div>
        )
    }

使用useMemo后,并将area作为依赖值传递进去,此时仅当area变化时才会重新执行getSquareArea()