React Hook的最全總結(附demo)

閱讀指南建議將每一個小demo都手動執行一遍,保證你會不枉此行。javascript

Hook是React 16.8(包括react-dom 16.8)新增的特性,它可讓你在不編寫class的狀況下使用state及其它的React特性,Hook是一個特殊的函數。html

React-router 從V5.1開始存在Hook方法並支持hook。java

React Redux 從 v7.1.0 開始支持 Hook API 並暴露了 useDispatchuseSelector 等 hook。react


接下來詳細的說說各個API,並給出demo,以爲不錯那就點個👍鼓勵一下。git

我的強烈建議:眼過千遍,不如手過一遍。github

1、Hook API各個擊破

1,useState

useState用來初始化state參數redux

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的函數複製代碼

二、useEffect

useEffect在函數組件中執行反作用(鉤子函數)的操做api

語法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>
    )
}複製代碼

---------------------------------------------------------


3、自定義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源碼

4、Hook使用規則

只在最頂層使用Hook

不要在循環,條件或嵌套函數中調用Hook;

只在組件函數中調用Hook

調用hook的地方

  • 在函數組件中調用hook
  • 在自定義hook中調用hook

版本升級

注意:要啓用Hook,全部React相關的package都必須升級到16.8.0或者更高的版本。不然,Hook將沒法運行。

參考資料

官方React-Hook

Hooks FAQ

相關文章
相關標籤/搜索