React Hook的最全总结(附demo,收藏一波)

 

阅读指南建议将每个小demo都手动执行一遍,保证你会不虚此行。

Hook是React 16.8(包括react-dom 16.8)新增的特性,它可以让你在不编写class的情况下使用state及其它的React特性,Hook是一个特殊的函数。

React-router 从V5.1开始存在Hook方法并支持hook。

React Redux 从 v7.1.0 开始支持 Hook API 并暴露了 useDispatchuseSelector 等 hook。

接下来详细的说说各个API,并给出demo,觉得不错那就点个👍鼓励一下。

个人强烈建议:眼过千遍,不如手过一遍。

 

一、Hook API各个击破 


1.useState

useState用来初始化state参数

import React from 'react';

function Example(){
    // 声明一个count的state变量
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>点击{count}次</p>
            <button onClick={() => setCount(count + 1)}>点击</button>
        </div>
    )
}

 useState功能:

  • 读取state:在class中即为this.state.count,在这里直接为count;
  • 更新state:在class中,通过this.setState()来更新count值,在这里使用setCount;
  • 方括号的作用:采用结构赋值,同时创建了两个变量count和setCount,等价于如下:
     
var demoData = useState(0);
var count = demoData[0];     // 第一个值为当前的state
var setCount = demoDate[1];  // 第二个值为更新state的函数

2.useEffect 

useEffect在函数组件中执行副作用(钩子函数)的操作

语法useEffect(() => {}, [])

接收两个参数,第一个函数,第二个为数组;

import React, { useState, useEffect } from 'React';

function Example(){
    const [count, setCount] = useState(0);
    
    const btnClick = () => {
        setCount(count + 1);
    }

    useEffect(() => {
        console.log('执行了useEffect');
        document.title = `点击了{count}次`
    })

    return (
    <div>
        <p>点击{count}次</p>
        <button onClick={() => { btnClick()}}>点击</button>
    </div>
    )
}

提示:熟悉React Class的生命周期函数,可以把useEffect Hook看做是componentDidMount,componentDidUpdate和componentWillUnmount这三个函数的组合。

useEffect在每次渲染页面后都执行。默认情况下,它在第一次渲染之后和每次更新之后都会执行;同一个函数组件中可以同时出现多个useEffect,React将按照useEffect声明的顺序依次调用组件中的每一个effect。

无需清除的effect

class的写法,即不需要在componentWillUnmount中执行操作

componentDidMount(){
    document.title = `点击了{count}次`;
}
componentDidUpdate(){
    document.title = `点击了{count}次`;
}

hook的写法

useEffect(() => {
    document.title = `点击了{count}次`;
})

需要清除的effect

即就是需要在componentWillUnmount中清除掉,例如我们使用setInterval来更新当前时间。代码如下

class FriendStatus extends React.Component{
    constructor(props){
        super(props);
        this.state = { nowTime: null};
        this.timer = null;
    }
    componentDidMount(){
        this.timer = setInterval(() => {
            this.setState({
                nowTime: new Date()
            })
        }, 1000)
    } 
    componentWillUnmount(){
        if (this.timer !== null) {
            clearInterval(timer);
        }
    }
    render(){
        let time = this.state.nowTime;
        return(
           <div>{time.toString()}</div>
        )
    }
}

使用hook,如下:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [nowTime, setNowTime] = useState(new Date());

  useEffect(() => {    
    let timer = setInterval(() => {
        setNowTime(new Date())
    }, 1000)
    return () => {   // 返回一个清理函数
      clearInterval(timer);
    };
  }, []);
  return(<div>{nowTime.toString()}</div>)
}

通过跳过Effect来进行性能优化

我们知道每次渲染后都执行清理或者执行effect会导致性能问题,在class中我们在componentDidUpdate中添加prevProps和prevState的比较逻辑来解决;

componentDidUpdate(prevProps, prevState){
    if(this.state.count !== prevState.count){
        document.title = `点击了{this.state.count}次`
    }
}

在hook中使用effect

useEffect(() => {
    console.log('执行了--useEffect')
    document.title = `点击了{count}次`;
}, [count]); // 在初次渲染和count发生变化时更新

如果第二数组参数为[],则Effect会在初次渲染执行一次及包含清除函数Effect再执行一次(可将上述代码中的[count]替换为[]测试);下面分别给出两种情况的执行代码:

// 带清除函数即为useEffect的第一个参数(函数)中再返回一个函数
// 不带清除函数+第二个参数为[];
// --> 整个生命周期只执行一次,相当于componentDidMount;
useEffect(() => {
    console.log('执行了--useEffect');
    document.title = `点击了${count}次`;
}, []);

// 带清除函数+第二个参数为[];
// --> 整个生命周期中执行了两次,相当于componentDidMount和componentWillUnmount;
useEffect(() => {
    console.log('执行了--useEffect');
    document.title = `点击了${count}次`; 
    return () => {  // 相当于componentWillUnmount;
        console.log('执行了--清除函数');
        document.title = "";   
    }
}, [])

如果不加第二个数组参数,则Effect除了会在初次渲染执行一次外,还会在每次更新都执行。

3,useContext

接收一个由React.createContext()创建的context对象(此处定义为Mycontext),并返回这个context属性vaule绑定的值;通过useContext获取到最近的<Mycontext.Provider value="">的props传递的value值;如下用法所示:

// Mycontext为React.createContext的返回值
const Mycontext = React.createContext();
<Mycontext.provider value={}>
    ...
</Mycontext.provider>
--------------------------
const value = useContext(Mycontext);

调用了useContext的组件总会在context值变化时重新渲染。

完整的demo如下:

import React, { useContext } from 'react';
const themes = {
  light: {
    color: '#ddd',
    background: 'yellow'
  },
  dark: {
    color: '#fff',
    background: '#333'
  }
}

const ThemeContext = React.createContext();

function TestContext(){
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Father></Father>
    </ThemeContext.Provider>
  )
}

function Child(){
  const theme = useContext(ThemeContext);
  return (
    <div style={{color: theme.color, backgroundColor: theme.background}}>测试一下useContext</div>
  )
}

function Father(){
  return (
    <div>
     <Child/>
     </div>
  )
}

export default TestContext;

4,useReducer

语法:

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

是useState的替代方案。在某些场景下,useReducer会比useState更适用。(熟悉Redux,就知道它怎么工作啦)

完整demo:

import React, { useReducer } from 'react';
const init = {
 count: 0
};

function reducer(state, action){
  switch(action.type){
    case 'add': 
     return {count: state.count + 1};
    case 'minus':
      return {count: state.count - 1};
    default: throw new Error();
  }
}

function TestReducer(){
  const [state, dispatch] = useReducer(reducer, init);
  return (
    <div>
      count: {state.count}
      <ul>
        <li><button onClick={() => dispatch({type: 'add'})}>+</button></li>
        <li><button onClick={() => dispatch({type: 'minus'})}>-</button></li>
      </ul>
    </div>
  )
}

export default TestReducer;

5,useRef

语法:

const refObj = useRef(initVal); 

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

完整demo:

import  React, { useRef } from 'react';
export default function TestUseRef(){
    // const InputEl = React.createRef(null);
    const InputEl = useRef(null);
    const getInputFocus = () => {
        InputEl.current.placeholder = "输入中";
        InputEl.current.focus();
    }
    return (
        <div>
            <input ref={InputEl} type="text" placeholder="请输入"/>
           <button onClick={() => getInputFocus()}>测试useRef</button>
        </div>
    )
}

测试发现跟React.createRef()功能相同。

6,useCallback

语法:返回一个回调函数;

const memoizedCallback = useCallback(() => {
    doSomething(a, b);
}, [a, b])

useCallback(fn, deps) 相当于useMemo(() => fn, deps),该回调函数仅在某个依赖项发生改变时才会更新。

完整demo:

import React, { useState, useCallback, useEffect } from 'react';
function TestUseCallback(){
  const [ count, setCount ] = useState(0);
  const [ inputVal, setInputVal ] = useState('');
  const backCount = useCallback(() => {
    return count;
  }, [count]);
  return (
    <div>
      <button onClick={() => {setCount(count + 1)}}>Click me--点击{count}次</button>
      <input value={inputVal} onChange={(e) => setInputVal(e.target.value)} />
      <hr/>
      <Child count={count} callback={backCount}/>
    </div>
  )
}

function Child(props){
  const {count, callback} = props;
  const [mycount, setMycount] = useState(() => callback());
  useEffect(() => {
    setMycount(callback());
  }, [callback]);

  return (
    <div>
      <h3>child组件</h3>
      <p>myCount---{mycount}</p>
    </div>
  )
}

export default TestUseCallback;

7,useMemo

useMemo接收一个自定义的函数和数组参数,仅在某个数组参数发生改变时才返回值并触发render,这种优化有助于避免在每次渲染时都进行高开销的计算;

语法:返回一个值;

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

不使用useMemo的完整demo:

import React, { useState, useMemo } from 'react';

function TestUseMemo(){
  const [count, setCount] = useState(0);
  const [value, setValue] = useState('');
  const AddSum = () => {
    console.log('addSum');
    let sum = 0;
    for(let i = 0; i < count*1000; i++){
      sum += i;
    } 
   return sum;
  }
  return (
    <div>
      <p>点击了{count}次</p>
      <p>计算求和所得{AddSum()}</p>
      <button onClick={() => {setCount(count + 1)}}>Click me</button>
      <input value={value} onChange={event => setValue(event.target.value)}/>
    </div>
  )
}

export default TestUseMemo;

发现无论是修改count,还是修改value,都会触发AddSum()函数进行计算;

使用useMemo的完整demo:

import React, { useState, useMemo } from 'react';

function TestUseMemo(){
  const [count, setCount] = useState(0);
  const [value, setValue] = useState('');
  const AddSum = useMemo(() => {
    console.log('useMemo');
    let sum = count;
    for(let i = 0; i < count*1000; i++){
      sum += i;
    } 
   return sum;
 }, [count]); // 只在count值发生变化时才重新计算值
 return (
    <div>
      <p>点击了{count}次</p>
      <p>计算求和所得{AddSum}</p>
      <button onClick={() => {setCount(count + 1)}}>Click me</button>
      <input value={value} onChange={event => setValue(event.target.value)}/>
    </div>
  )
}

export default TestUseMemo;

通过使用useMemo,我们发现只有在count值发生变化时才重新执行AddSum进行计算并重新渲染,从而避免了不必要的性能开销。

8,useLayoutEffect

语法与useEffect一样,但是与useEffect的执行时机不一样;

完整demo验证:

import React, { useState, useEffect, useRef, useLayoutEffect } from 'react';

export default function TestuseLayout(){
    const [count, setCount] = useState(0);
    const [text, setText] = useState('');
    const InputRef = useRef(null);
    useEffect(() => {
        console.log(InputRef, 'start useEffect');
        document.title = `${count} times`;
        return () => {
            console.log(InputRef, 'end useEffect');
            document.title = 'remove';
        }
    });
    useLayoutEffect(() => {
        console.log(InputRef, 'start useLayoutEffect');
        document.title = `${count} times`;
        return () => {
            console.log(InputRef, 'end useLayoutEffect');
            document.title = 'Layout remove';
        }
    })
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>Click me</button>
            <p>
                <input ref={InputRef} value={text} onChange={(e) => setText(e.target.value)}/>
            </p>
        </div>
    )
}

看执行结果如下:

结论:

 

  • useLayoutEffect的执行先于useEffect;
  • 第一个函数参数的返回函数执行先于初始函数;

 

9,useImperativeHandle

useImperativeHandle可以让你在使用ref时将自定义的实例值暴露给父组件,应尽量避免使用ref,useImperativeHandle应当与forwardRef一起使用。

语法:

useImperativeHandle(ref, createHandle, [deps]);

完整demo:

import React, { useImperativeHandle, useRef, forwardRef} from 'react';

function TestuseImper(){
   const childRef = useRef(null);
   const btnFn = () => {
      console.log('childRef=', childRef);
      childRef.current.focus();
  }
  return (
    <div>
      <button onClick={() => {btnFn()}}>获取子组件input的焦点</button>
      <hr/>
      <Child ref = {childRef}/>
    </div>
  )
}

function Child(props, ref){
  const InputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => InputRef.current.focus();
  }))

  return (
    <div>
      <input ref={InputRef}/>
    </div>
  )
}

Child = forwardRef(Child);

export default TestuseImper;

上述demo执行后,我们发现父组件可以通过暴露的ref来获取子组件的ref,并进行修改。

10,useDebugValue

语法:

useDebugValue(value)

完整demo:

import React, { useState, useDebugValue } from 'react';

export default function TestuseLayout(){
    function useMyCount(num) {
        const [ count, setCount ] = useState(0);
        useDebugValue(count > num ? '溢出' : '不足');
        const myCount = () => {
          setCount(count + 2);
        }
        return [ count, myCount ];
    }
    const [count , myCount] = useMyCount(10);
    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => myCount(count + 1)}>Click me</button>
        </div>
    )
}

二、自定义Hook


自定义Hook可以让组件之间复用一些状态逻辑,在class方式下通常采用高阶组件来实现,而自定义Hook可以让我们在不增加组件的情况下达到同样的目的,可以将组件逻辑提取到可重用的函数中;下面提供一个简单的自定义hook完整demo:

import React, { useState } from 'react';

function useChangeCount(init){
  // 自定义hook,名字必须以use开头
  const [count, setCount] = useState(init);
  const plus = () => {
    setCount(count + 1);
  }
  const minus = () => {
    setCount(count - 1);
  };
  const reset = () => {
    setCount(init);
  };
  
  return [count, {plus, minus, reset}];
}

function SelfHook(){
  const [count, setCount] = useChangeCount(0);
  return (
    <div>
      <p>当前count: {count}</p>
      <div><button onClick={setCount.plus}>增加</button></div>
      <div><button onClick={setCount.minus}>减少</button></div>
      <div><button onClick={setCount.reset}>重置</button></div>
    </div>
  )
}

export default SelfHook;

更多自定义Hook可阅读 React-use源码

三、Hook使用规则


只在最顶层使用Hook

不要在循环,条件或嵌套函数中调用Hook;

只在组件函数中调用Hook

调用hook的地方

  • 在函数组件中调用hook
  • 在自定义hook中调用hook

版本升级

注意:要启用Hook,所有React相关的package都必须升级到16.8.0或者更高的版本。否则,Hook将无法运行。

参考资料


官方React-Hook

Hooks FAQ

  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值