React Hooks 詳解(近 1w 字)+ 項目實戰

React Hooks

1、什麼是 Hooks

  • React 一直都提倡使用函數組件,可是有時候須要使用 state 或者其餘一些功能時,只能使用類組件,由於函數組件沒有實例,沒有生命週期函數,只有類組件纔有
  • Hooks 是 React 16.8 新增的特性,它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性
  • 若是你在編寫函數組件並意識到須要向其添加一些 state,之前的作法是必須將其它轉化爲 class。如今你能夠直接在現有的函數組件中使用 Hooks
  • 凡是 use 開頭的 React API 都是 Hooks

2、Hooks 解決的問題

1. 類組件的不足

  • 狀態邏輯難複用: 在組件之間複用狀態邏輯很難,可能要用到 render props渲染屬性)或者 HOC高階組件),但不管是渲染屬性,仍是高階組件,都會在原先的組件外包裹一層父容器(通常都是 div 元素),致使層級冗餘
  • 趨向複雜難以維護:
    • 在生命週期函數中混雜不相干的邏輯(如:在 componentDidMount 中註冊事件以及其餘的邏輯,在 componentWillUnmount 中卸載事件,這樣分散不集中的寫法,很容易寫出 bug )
    • 類組件中處處都是對狀態的訪問和處理,致使組件難以拆分紅更小的組件
  • this 指向問題:父組件給子組件傳遞函數時,必須綁定 this
    • react 中的組件四種綁定 this 方法的區別
class App extends React.Component<any, any> {
    handleClick2;

    constructor(props) {
        super(props);
        this.state = {
            num: 1,
            title: ' react study'
        };
        this.handleClick2 = this.handleClick1.bind(this);
    }

    handleClick1() {
        this.setState({
            num: this.state.num + 1,
        })
    }

    handleClick3 = () => {
        this.setState({
            num: this.state.num + 1,
        })
    };

    render() {
        return (<div>
            <h2>Ann, {this.state.num}</h2>
            <button onClick={this.handleClick2}>btn1</button>
            <button onClick={this.handleClick1.bind(this)}>btn2</button>
            <button onClick={() => this.handleClick1()}>btn3</button>
            <button onClick={this.handleClick3}>btn4</button>
        </div>)
    }
}
複製代碼

前提:子組件內部作了性能優化,如(React.PureComponentjavascript

  • 第一種是在構造函數中綁定 this:那麼每次父組件刷新的時候,若是傳遞給子組件其餘的 props 值不變,那麼子組件就不會刷新;
  • 第二種是在 render() 函數裏面綁定 this:由於 bind 函數會返回一個新的函數,因此每次父組件刷新時,都會從新生成一個函數,即便父組件傳遞給子組件其餘的 props 值不變,子組件每次都會刷新;
  • 第三種是使用箭頭函數:父組件刷新的時候,即便兩個箭頭函數的函數體是同樣的,都會生成一個新的箭頭函數,因此子組件每次都會刷新;
  • 第四種是使用類的靜態屬性:原理和第一種方法差很少,比第一種更簡潔

綜上所述,若是不注意的話,很容易寫成第三種寫法,致使性能上有所損耗。html

2. Hooks 優點

  • 能優化類組件的三大問題
  • 能在無需修改組件結構的狀況下複用狀態邏輯(自定義 Hooks )
  • 能將組件中相互關聯的部分拆分紅更小的函數(好比設置訂閱或請求數據)
  • 反作用的關注點分離反作用指那些沒有發生在數據向視圖轉換過程當中的邏輯,如 ajax 請求、訪問原生dom 元素、本地持久化緩存、綁定/解綁事件、添加訂閱、設置定時器、記錄日誌等。以往這些反作用都是寫在類組件生命週期函數中的。而 useEffect 在所有渲染完畢後纔會執行,useLayoutEffect 會在瀏覽器 layout 以後,painting 以前執行。

3、注意事項

4、useState & useMemo & useCallback

  • React 假設當你屢次調用 useState 的時候,你能保證每次渲染時它們的調用順序是不變的。
  • 經過在函數組件裏調用它來給組件添加一些內部 state,React會 在重複渲染時保留這個 state
  • useState 惟一的參數就是初始 state
  • useState 會返回一個數組一個 state,一個更新 state 的函數
    • 在初始化渲染期間,返回的狀態 (state) 與傳入的第一個參數 (initialState) 值相同
    • 你能夠在事件處理函數中或其餘一些地方調用這個函數。它相似 class 組件的 this.setState,可是它不會把新的 state 和舊的 state 進行合併,而是直接替換
// 這裏能夠任意命名,由於返回的是數組,數組解構
const [state, setState] = useState(initialState);
複製代碼

4.1 使用例子

import React, { useState } from "react";
import ReactDOM from "react-dom";

function Child1(porps) {
  console.log(porps);
  const { num, handleClick } = porps;
  return (
    <div onClick={() => { handleClick(num + 1); }} > child </div>
  );
}

function Child2(porps) {
  // console.log(porps);
  const { text, handleClick } = porps;
  return (
    <div>
      child2
      <Grandson text={text} handleClick={handleClick} />
    </div>
  );
}

function Grandson(porps) {
  console.log(porps);
  const { text, handleClick } = porps;
  return (
    <div
      onClick={() => {
        handleClick(text + 1);
      }}
    >
      grandson
    </div>
  );
}

function Parent() {
  let [num, setNum] = useState(0);
  let [text, setText] = useState(1);

  return (
    <div>
      <Child1 num={num} handleClick={setNum} />
      <Child2 text={text} handleClick={setText} />
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Parent />, rootElement);
複製代碼

4.2 每次渲染都是獨立的閉包

  • 每一次渲染都有它本身的 Props 和 State
  • 每一次渲染都有它本身的事件處理函數
  • 當點擊更新狀態的時候,函數組件都會從新被調用,那麼每次渲染都是獨立的,取到的值不會受後面操做的影響
function Counter2(){
  let [number,setNumber] = useState(0);
  function alertNumber(){
    setTimeout(()=>{
      // alert 只能獲取到點擊按鈕時的那個狀態
      alert(number);
    },3000);
  }
  return (
      <> <p>{number}</p> <button onClick={()=>setNumber(number+1)}>+</button> <button onClick={alertNumber}>alertNumber</button> </> ) } 複製代碼

4.3 函數式更新

  • 若是新的 state 須要經過使用先前的 state 計算得出,那麼能夠將回調函數當作參數傳遞給 setState。該回調函數將接收先前的 state,並返回一個更新後的值。
function Counter(){
    let [number,setNumber] = useState(0);
    function lazy(){
        setTimeout(() => {
            // setNumber(number+1);
            // 這樣每次執行時都會去獲取一遍 state,而不是使用點擊觸發時的那個 state
            setNumber(number=>number+1);
        }, 3000);
    }
    return (
        <> <p>{number}</p> <button onClick={()=>setNumber(number+1)}>+</button> <button onClick={lazy}>lazy</button> </> ) } 複製代碼

4.4 惰性初始化 state

  • initialState 參數只會在組件的初始化渲染中起做用,後續渲染時會被忽略
  • 若是初始 state 須要經過複雜計算得到,則能夠傳入一個函數,在函數中計算並返回初始的 state,此函數只在初始渲染時被調用
function Counter5(props){
    console.log('Counter5 render');
    // 這個函數只在初始渲染時執行一次,後續更新狀態從新渲染組件時,該函數就不會再被調用
    function getInitState(){
        return {number:props.number};
    }
    let [counter,setCounter] = useState(getInitState);
    return (
        <> <p>{counter.number}</p> <button onClick={()=>setCounter({number:counter.number+1})}>+</button> <button onClick={()=>setCounter(counter)}>setCounter</button> </> ) } 複製代碼

4.5 性能優化

4.5.1 Object.is (淺比較)

  • Hook 內部使用 Object.is 來比較新/舊 state 是否相等
  • 與 class 組件中的 setState 方法不一樣,若是你修改狀態的時候,傳的狀態值沒有變化,則不從新渲染
  • 與 class 組件中的 setState 方法不一樣,useState 不會自動合併更新對象。你能夠用函數式的 setState 結合展開運算符來達到合併更新對象的效果
function Counter(){
    const [counter,setCounter] = useState({name:'計數器',number:0});
    console.log('render Counter')
    // 若是你修改狀態的時候,傳的狀態值沒有變化,則不從新渲染
    return (
        <> <p>{counter.name}:{counter.number}</p> <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button> <button onClick={()=>setCounter(counter)}>++</button> </> ) } 複製代碼

4.5.2 減小渲染次數

  • 默認狀況,只要父組件狀態變了(無論子組件依不依賴該狀態),子組件也會從新渲染
  • 通常的優化:
    1. 類組件:可使用 pureComponent
    2. 函數組件:使用 React.memo ,將函數組件傳遞給 memo 以後,就會返回一個新的組件,新組件的功能:若是接受到的屬性不變,則不從新渲染函數
  • 可是怎麼保證屬性不會變尼?這裏使用 useState ,每次更新都是獨立的const [number,setNumber] = useState(0) 也就是說每次都會生成一個新的值(哪怕這個值沒有變化),即便使用了 React.memo ,也仍是會從新渲染
import React,{useState,memo,useMemo,useCallback} from 'react';

function SubCounter({onClick,data}){
    console.log('SubCounter render');
    return (
        <button onClick={onClick}>{data.number}</button>
    )
}
SubCounter = memo(SubCounter);
export  default  function Counter6(){
    console.log('Counter render');
    const [name,setName]= useState('計數器');
    const [number,setNumber] = useState(0);
    const data ={number};
    const addClick = ()=>{
        setNumber(number+1);
    };
    return (
        <>
            <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
            <SubCounter data={data} onClick={addClick}/>
        </>
    )
}
複製代碼
  • 更深刻的優化:
    1. useCallback:接收一個內聯回調函數參數和一個依賴項數組(子組件依賴父組件的狀態,即子組件會使用到父組件的值) ,useCallback 會返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新
    2. useMemo:把建立函數和依賴項數組做爲參數傳入 useMemo,它僅會在某個依賴項改變時才從新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算
import React,{useState,memo,useMemo,useCallback} from 'react';

function SubCounter({onClick,data}){
    console.log('SubCounter render');
    return (
        <button onClick={onClick}>{data.number}</button>
    )
}
SubCounter = memo(SubCounter);

let oldData,oldAddClick;
export  default  function Counter2(){
    console.log('Counter render');
    const [name,setName]= useState('計數器');
    const [number,setNumber] = useState(0);
    // 父組件更新時,這裏的變量和函數每次都會從新建立,那麼子組件接受到的屬性每次都會認爲是新的
    // 因此子組件也會隨之更新,這時候能夠用到 useMemo
    // 有沒有後面的依賴項數組很重要,不然仍是會從新渲染
    // 若是後面的依賴項數組沒有值的話,即便父組件的 number 值改變了,子組件也不會去更新
    //const data = useMemo(()=>({number}),[]);
    const data = useMemo(()=>({number}),[number]);
    console.log('data===oldData ',data===oldData);
    oldData = data;
    
    // 有沒有後面的依賴項數組很重要,不然仍是會從新渲染
    const addClick = useCallback(()=>{
        setNumber(number+1);
    },[number]);
    console.log('addClick===oldAddClick ',addClick===oldAddClick);
    oldAddClick=addClick;
    return (
        <>
            <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>
            <SubCounter data={data} onClick={addClick}/>
        </>
    )
}
複製代碼

4.6 useState 源碼中的鏈表實現

import React from 'react';
import ReactDOM from 'react-dom';

let firstWorkInProgressHook = {memoizedState: null, next: null};
let workInProgressHook;

function useState(initState) {
    let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null};

    function setState(newState) {
        currentHook.memoizedState = newState;
        render();
    }
  	// 這就是爲何 useState 書寫順序很重要的緣由
		// 假如某個 useState 沒有執行,會致使指針移動出錯,數據存取出錯
    if (workInProgressHook.next) {
        // 這裏只有組件刷新的時候,纔會進入
        // 根據書寫順序來取對應的值
        // console.log(workInProgressHook);
        workInProgressHook = workInProgressHook.next;
    } else {
        // 只有在組件初始化加載時,纔會進入
        // 根據書寫順序,存儲對應的數據
        // 將 firstWorkInProgressHook 變成一個鏈表結構
        workInProgressHook.next = currentHook;
        // 將 workInProgressHook 指向 {memoizedState: initState, next: null}
        workInProgressHook = currentHook;
        // console.log(firstWorkInProgressHook);
    }
    return [currentHook.memoizedState, setState];
}

function Counter() {
    // 每次組件從新渲染的時候,這裏的 useState 都會從新執行
    const [name, setName] = useState('計數器');
    const [number, setNumber] = useState(0);
    return (
        <> <p>{name}:{number}</p> <button onClick={() => setName('新計數器' + Date.now())}>新計數器</button> <button onClick={() => setNumber(number + 1)}>+</button> </> ) } function render() { // 每次從新渲染的時候,都將 workInProgressHook 指向 firstWorkInProgressHook workInProgressHook = firstWorkInProgressHook; ReactDOM.render(<Counter/>, document.getElementById('root')); } render(); 複製代碼

5、useReducer

  • useReducer 和 redux 中 reducer 很像
  • useState 內部就是靠 useReducer 來實現的
  • useState 的替代方案,它接收一個形如 (state, action) => newState 的 reducer,並返回當前的 state 以及與其配套的 dispatch 方法
  • 在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較複雜且包含多個子值,或者下一個 state 依賴於以前的 state 等
let initialState = 0;
// 若是你但願初始狀態是一個{number:0}
// 能夠在第三個參數中傳遞一個這樣的函數 ()=>({number:initialState})
// 這個函數是一個惰性初始化函數,能夠用來進行復雜的計算,而後返回最終的 initialState
const [state, dispatch] = useReducer(reducer, initialState, init);
複製代碼
const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter(){
    const [state, dispatch] = useReducer(reducer, initialState,init);
    return (
        <> Count: {state.number} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ) } 複製代碼

6、useContext

  • 接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值
  • 當前的 context 值由上層組件中距離當前組件最近的 <MyContext.Provider> 的 value prop 決定
  • 當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值
  • useContext(MyContext) 至關於 class 組件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是讓你可以讀取 context 的值以及訂閱 context 的變化。你仍然須要在上層組件樹中使用 <MyContext.Provider> 來爲下層組件提供 context
import React,{useState,memo,useMemo,useCallback,useReducer,createContext,useContext} from 'react';
import ReactDOM from 'react-dom';

const initialState = 0;
function reducer(state=initialState,action){
    switch(action.type){
        case 'ADD':
            return {number:state.number+1};
        default:
            break; 
    }
}

const CounterContext = createContext();
// 第一種獲取 CounterContext 方法:不使用 hook
function SubCounter_one(){
    return (
        <CounterContext.Consumer>
            {
                value=>(
                    <>
                    <p>{value.state.number}</p>
                    <button onClick={()=>value.dispatch({type:'ADD'})}>+</button>
                    </>
                )
            }
         
        </CounterContext.Consumer>
    )
}
// 第二種獲取 CounterContext 方法:使用 hook ,更簡潔
function SubCounter(){
    const {state, dispatch} = useContext(CounterContext);
    return (
        <>
            <p>{state.number}</p>
            <button onClick={()=>dispatch({type:'ADD'})}>+</button>
        </>
    )
}
/* class SubCounter extends React.Component{
    static contextTypes = CounterContext
    this.context =  {state, dispatch}
} */

function Counter(){
    const [state, dispatch] = useReducer((reducer), initialState, ()=>({number:initialState}));
    return (
        <CounterContext.Provider value={{state, dispatch}}>
            <SubCounter/>
        </CounterContext.Provider>
    )
}
ReactDOM.render(<Counter  />, document.getElementById('root'));

複製代碼

7、useEffect

  • effect(反作用):指那些沒有發生在數據向視圖轉換過程當中的邏輯,如 ajax 請求、訪問原生dom 元素、本地持久化緩存、綁定/解綁事件、添加訂閱、設置定時器、記錄日誌等。
  • 反作用操做能夠分兩類:須要清除的和不須要清除的
  • 原先在函數組件內(這裏指在 React 渲染階段)改變 dom 、發送 ajax 請求以及執行其餘包含反作用的操做都是不被容許的,由於這可能會產生莫名其妙的 bug 並破壞 UI 的一致性
  • useEffect 就是一個 Effect Hook,給函數組件增長了操做反作用的能力。它跟 class 組件中的 componentDidMountcomponentDidUpdatecomponentWillUnmount 具備相同的用途,只不過被合併成了一個 API
  • useEffect 接收一個函數,該函數會在組件渲染到屏幕以後才執行,該函數有要求:要麼返回一個能清除反作用的函數,要麼就不返回任何內容
  • componentDidMountcomponentDidUpdate 不一樣,使用 useEffect 調度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快。大多數狀況下,effect 不須要同步地執行。在個別狀況下(例如測量佈局),有單獨的 useLayoutEffect Hook 供你使用,其 API 與 useEffect 相同。

7.1 使用 class 組件實現修改標題

  • 在這個 class 中,咱們須要在兩個生命週期函數中編寫重複的代碼,這是由於不少狀況下,咱們但願在組件加載和更新時執行一樣的操做。咱們但願它在每次渲染以後執行,但 React 的 class 組件沒有提供這樣的方法。即便咱們提取出一個方法,咱們仍是要在兩個地方調用它。而 useEffect 會在第一次渲染以後和每次更新以後都會執行
class Counter extends React.Component{
    state = {number:0};
    add = ()=>{
        this.setState({number:this.state.number+1});
    };
    componentDidMount(){
        this.changeTitle();
    }
    componentDidUpdate(){
        this.changeTitle();
    }
    changeTitle = ()=>{
        document.title = `你已經點擊了${this.state.number}次`;
    };
    render(){
        return (
            <> <p>{this.state.number}</p> <button onClick={this.add}>+</button> </> ) } } 複製代碼

7.2 使用 useEffect 來實現修改標題

  • 每次咱們從新渲染,都會生成新的 effect,替換掉以前的。某種意義上講,effect 更像是渲染結果的一部分 —— 每一個 effect 屬於一次特定的渲染。
import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
    const [number,setNumber] = useState(0);
    // useEffect裏面的這個函數會在第一次渲染以後和更新完成後執行
    // 至關於 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        document.title = `你點擊了${number}次`;
    });
    return (
        <> <p>{number}</p> <button onClick={()=>setNumber(number+1)}>+</button> </> ) } ReactDOM.render(<Counter />, document.getElementById('root')); 複製代碼

7.3 清除反作用

  • 反作用函數還能夠經過返回一個函數來指定如何清除反作用,爲防止內存泄漏,清除函數會在組件卸載前執行。若是組件屢次渲染,則在執行下一個 effect 以前,上一個 effect 就已被清除。
function Counter(){
    let [number,setNumber] = useState(0);
    let [text,setText] = useState('');
    // 至關於componentDidMount 和 componentDidUpdate
    useEffect(()=>{
        console.log('開啓一個新的定時器')
        let $timer = setInterval(()=>{
            setNumber(number=>number+1);
        },1000);
        // useEffect 若是返回一個函數的話,該函數會在組件卸載和更新時調用
        // useEffect 在執行反作用函數以前,會先調用上一次返回的函數
        // 若是要清除反作用,要麼返回一個清除反作用的函數
       /* return ()=>{ console.log('destroy effect'); clearInterval($timer); } */
    });
    // },[]);//要麼在這裏傳入一個空的依賴項數組,這樣就不會去重複執行
    return (
        <>
          <input value={text} onChange={(event)=>setText(event.target.value)}/>
          <p>{number}</p>
          <button>+</button>
        </>
    )
}
複製代碼

7.4 跳過 effect 進行性能優化

  • 依賴項數組控制着 useEffect 的執行
  • 若是某些特定值在兩次重渲染之間沒有發生變化,你能夠通知 React 跳過對 effect 的調用,只要傳遞數組做爲 useEffect 的第二個可選參數便可
  • 若是想執行只運行一次的 effect(僅在組件掛載和卸載時執行),能夠傳遞一個空數組([])做爲第二個參數。這就告訴 React 你的 effect 不依賴於 props 或 state 中的任何值,因此它永遠都不須要重複執行
  • 推薦啓用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。
function Counter(){
    let [number,setNumber] = useState(0);
    let [text,setText] = useState('');
    // 至關於componentDidMount 和 componentDidUpdate
    useEffect(()=>{
        console.log('useEffect');
        let $timer = setInterval(()=>{
            setNumber(number=>number+1);
        },1000);
    },[text]);// 數組表示 effect 依賴的變量,只有當這個變量發生改變以後纔會從新執行 efffect 函數
    return (
        <>
          <input value={text} onChange={(event)=>setText(event.target.value)}/>
          <p>{number}</p>
          <button>+</button>
        </>
    )
}
複製代碼

7.5 使用多個 Effect 實現關注點分離

  • 使用 Hook 其中一個目的就是要解決 class 中生命週期函數常常包含不相關的邏輯,但又把相關邏輯分離到了幾個不一樣方法中的問題。
// 類組件版
class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
複製代碼
  • 能夠發現設置 document.title 的邏輯是如何被分割到 componentDidMountcomponentDidUpdate 中的,訂閱邏輯又是如何被分割到 componentDidMountcomponentWillUnmount 中的。並且 componentDidMount 中同時包含了兩個不一樣功能的代碼。這樣會使得生命週期函數很混亂。java

  • Hook 容許咱們按照代碼的用途分離他們, 而不是像生命週期函數那樣。React 將按照 effect 聲明的順序依次調用組件中的 每個 effect。react

// Hooks 版
function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}
複製代碼

8、useLayoutEffect

  • useEffect 在所有渲染完畢後纔會執行
  • useLayoutEffect 會在 瀏覽器 layout 以後,painting 以前執行
  • 其函數簽名與 useEffect 相同,但它會在全部的 DOM 變動以後同步調用 effect
  • 可使用它來讀取 DOM 佈局並同步觸發重渲染
  • 在瀏覽器執行繪製以前 useLayoutEffect 內部的更新計劃將被同步刷新
  • 儘量使用標準的 useEffect 以免阻塞視圖更新
function LayoutEffect() {
    const [color, setColor] = useState('red');
    useLayoutEffect(() => {
        alert(color);
    });
    useEffect(() => {
        console.log('color', color);
    });
    return (
        <> <div id="myDiv" style={{ background: color }}>顏色</div> <button onClick={() => setColor('red')}>紅</button> <button onClick={() => setColor('yellow')}>黃</button> <button onClick={() => setColor('blue')}>藍</button> </> ); } 複製代碼

9、useRef & useImperativeHandle

8.1 useRef

  • 類組件、React 元素用 React.createRef,函數組件使用 useRef
  • useRef 返回一個可變的 ref 對象,其 current 屬性被初始化爲傳入的參數(initialValue)
const refContainer = useRef(initialValue);
複製代碼
  • useRef 返回的 ref 對象在組件的整個生命週期內保持不變,也就是說每次從新渲染函數組件時,返回的ref 對象都是同一個(使用 React.createRef ,每次從新渲染組件都會從新建立 ref)
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
    let [number, setNumber] = useState(0);
    return (
        <>
            <Child />
            <button onClick={() => setNumber({ number: number + 1 })}>+</button>
        </>
    )
}
let input;
function Child() {
    const inputRef = useRef();
    console.log('input===inputRef', input === inputRef);
    input = inputRef;
    function getFocus() {
        inputRef.current.focus();
    }
    return (
        <>
            <input type="text" ref={inputRef} />
            <button onClick={getFocus}>得到焦點</button>
        </>
    )
}
ReactDOM.render(<Parent />, document.getElementById('root'));
複製代碼

8.2 forwardRef

  • 由於函數組件沒有實例,因此函數組件沒法像類組件同樣能夠接收 ref 屬性
function Parent() {
    return (
        <>
         // <Child ref={xxx} /> 這樣是不行的
            <Child />
            <button>+</button>
        </>
    )
}
複製代碼
  • forwardRef 能夠在父組件中操做子組件的 ref 對象
  • forwardRef 能夠將父組件中的 ref 對象轉發到子組件中的 dom 元素上
  • 子組件接受 props 和 ref 做爲參數
function Child(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}
Child = React.forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  // 在使用類組件的時候,建立 ref 返回一個對象,該對象的 current 屬性值爲空
  // 只有當它被賦給某個元素的 ref 屬性時,纔會有值
  // 因此父組件(類組件)建立一個 ref 對象,而後傳遞給子組件(類組件),子組件內部有元素使用了
  // 那麼父組件就能夠操做子組件中的某個元素
  // 可是函數組件沒法接收 ref 屬性 <Child ref={xxx} /> 這樣是不行的
  // 因此就須要用到 forwardRef 進行轉發
  const inputRef = useRef();//{current:''}
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <ForwardChild ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>得到焦點</button>
      </>
  )
}
複製代碼

8.3 useImperativeHandle

  • useImperativeHandle可讓你在使用 ref 時,自定義暴露給父組件的實例值,不能讓父組件想幹嗎就幹嗎
  • 在大多數狀況下,應當避免使用 ref 這樣的命令式代碼。useImperativeHandle 應當與 forwardRef 一塊兒使用
  • 父組件可使用操做子組件中的多個 ref
import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';

function Child(props,parentRef){
    // 子組件內部本身建立 ref 
    let focusRef = useRef();
    let inputRef = useRef();
    useImperativeHandle(parentRef,()=>(
      // 這個函數會返回一個對象
      // 該對象會做爲父組件 current 屬性的值
      // 經過這種方式,父組件可使用操做子組件中的多個 ref
        return {
            focusRef,
            inputRef,
            name:'計數器',
            focus(){
                focusRef.current.focus();
            },
            changeText(text){
                inputRef.current.value = text;
            }
        }
    });
    return (
        <>
            <input ref={focusRef}/>
            <input ref={inputRef}/>
        </>
    )

}
Child = forwardRef(Child);
function Parent(){
  const parentRef = useRef();//{current:''}
  function getFocus(){
    parentRef.current.focus();
    // 由於子組件中沒有定義這個屬性,實現了保護,因此這裏的代碼無效
    parentRef.current.addNumber(666);
    parentRef.current.changeText('<script>alert(1)</script>');
    console.log(parentRef.current.name);
  }
  return (
      <>
        <ForwardChild ref={parentRef}/>
        <button onClick={getFocus}>得到焦點</button>
      </>
  )
}
複製代碼

10、自定義 Hook

  • 自定義 Hook 更像是一種約定,而不是一種功能。若是函數的名字以 use 開頭,而且調用了其餘的 Hook,則就稱其爲一個自定義 Hook
  • 有時候咱們會想要在組件之間重用一些狀態邏輯,以前要麼用 render props ,要麼用高階組件,要麼使用 redux
  • 自定義 Hook 可讓你在不增長組件的狀況下達到一樣的目的
  • Hook 是一種複用狀態邏輯的方式,它不復用 state 自己
  • 事實上 Hook 的每次調用都有一個徹底獨立的 state
import React, { useLayoutEffect, useEffect, useState } from 'react';
import ReactDOM from 'react-dom';

function useNumber(){
  let [number,setNumber] = useState(0);
  useEffect(()=>{
    setInterval(()=>{
        setNumber(number=>number+1);
    },1000);
  },[]);
  return [number,setNumber];
}
// 每一個組件調用同一個 hook,只是複用 hook 的狀態邏輯,並不會共用一個狀態
function Counter1(){
    let [number,setNumber] = useNumber();
    return (
        <div><button onClick={()=>{ setNumber(number+1) }}>{number}</button></div>
    )
}
function Counter2(){
    let [number,setNumber] = useNumber();
    return (
        <div><button onClick={()=>{ setNumber(number+1) }}>{number}</button></div>
    )
}
ReactDOM.render(<><Counter1 /><Counter2 /></>, document.getElementById('root')); 複製代碼

11、常見問題

1. 使用 eslint-plugin-react-hooks 來檢查代碼錯誤,給出提示

{
  "plugins": ["react-hooks"],
  // ...
  "rules": {
    "react-hooks/rules-of-hooks": 'error',// 檢查 Hook 的規則
    "react-hooks/exhaustive-deps": 'warn' // 檢查 effect 的依賴
  }
}
複製代碼

2.爲何每次更新的時候都要運行 Effect

react.docschina.org/docs/hooks-…ios

3.爲何必須在組件的頂層使用 Hook & 在單個組件中使用多個 State Hook 或 Effect Hook,那麼 React 怎麼知道哪一個 state 對應哪一個 useState?

  • React 依賴於 Hook 的調用順序,若是能確保 Hook 在每一次渲染中都按照一樣的順序被調用。那麼React 可以在屢次的 useStateuseEffect 調用之間保持 hook 狀態的正確性
function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}
複製代碼
// ------------
// 首次渲染
// ------------
useState('Mary')           // 1. 使用 'Mary' 初始化變量名爲 name 的 state
useEffect(persistForm)     // 2. 添加 effect 以保存 form 操做
useState('Poppins')        // 3. 使用 'Poppins' 初始化變量名爲 surname 的 state
useEffect(updateTitle)     // 4. 添加 effect 以更新標題
// -------------
// 二次渲染
// -------------
useState('Mary')           // 1. 讀取變量名爲 name 的 state(參數被忽略)
useEffect(persistForm)     // 2. 替換保存 form 的 effect
useState('Poppins')        // 3. 讀取變量名爲 surname 的 state(參數被忽略)
useEffect(updateTitle)     // 4. 替換更新標題的 effect
// ...
複製代碼

只要 Hook 的調用順序在屢次渲染之間保持一致,React 就能正確地將內部 state 和對應的 Hook 進行關聯。但若是咱們將一個 Hook (例如 persistForm effect) 調用放到一個條件語句中會發生什麼呢?git

// 🔴 在條件語句中使用 Hook 違反第一條規則
  if (name !== '') {
    useEffect(function persistForm() {
      localStorage.setItem('formData', name);
    });
  }
複製代碼

在第一次渲染中 name !== '' 這個條件值爲 true,因此咱們會執行這個 Hook。可是下一次渲染時咱們可能清空了表單,表達式值變爲 false。此時的渲染會跳過該 Hook,Hook 的調用順序發生了改變:github

useState('Mary')           // 1. 讀取變量名爲 name 的 state(參數被忽略)
// useEffect(persistForm) // 🔴 此 Hook 被忽略!
useState('Poppins')        // 🔴 2 (以前爲 3)。讀取變量名爲 surname 的 state 失敗
useEffect(updateTitle)     // 🔴 3 (以前爲 4)。替換更新標題的 effect 失敗
複製代碼

React 不知道第二個 useState 的 Hook 應該返回什麼。React 會覺得在該組件中第二個 Hook 的調用像上次的渲染同樣,對應得是 persistForm 的 effect,但並不是如此。從這裏開始,後面的 Hook 調用都被提早執行,致使 bug 的產生。ajax

若是咱們想要有條件地執行一個 effect,能夠將判斷放到 Hook 的_內部_:typescript

useEffect(function persistForm() {
    // 👍 將條件判斷放置在 effect 中
    if (name !== '') {
      localStorage.setItem('formData', name);
    }
  });
複製代碼

4. 自定義 Hook 必須以 use 開頭嗎?

必須如此。這個約定很是重要。不遵循的話,因爲沒法判斷某個函數是否包含對其內部 Hook 的調用,React 將沒法自動檢查你的 Hook 是否違反了 Hook 的規則npm

5. 在兩個組件中使用相同的 Hook 會共享 state 嗎?

不會。自定義 Hook 是一種重用_狀態邏輯_的機制(例如設置爲訂閱並存儲當前值),因此每次使用自定義 Hook 時,其中的全部 state 和反作用都是徹底隔離的。

6. 在一個組件中屢次調用 useState 或者 useEffect,每次調用 Hook,它都會獲取獨立的 state,是徹底獨立的。

7. 當組件擁有多個 state 時,應該把多個 state 合併成一個 state ,仍是把 state 切分紅多個 state 變量?

react.docschina.org/docs/hooks-…

  • 要麼把全部 state 都放在同一個 useState 調用中,要麼每個字段都對應一個 useState 調用,這兩方式都能跑通。
  • 當你在這兩個極端之間找到平衡,而後把相關 state 組合到幾個獨立的 state 變量時,組件就會更加的可讀。若是 state 的邏輯開始變得複雜,咱們推薦用 useReducer 來管理它,或使用自定義 Hook。

8. 能夠只在更新時運行 effect 嗎?

這是個比較罕見的使用場景。若是你須要的話,你能夠 使用一個可變的 ref 手動存儲一個布爾值來表示是首次渲染仍是後續渲染,而後在你的 effect 中檢查這個標識。(若是你發現本身常常在這麼作,你能夠爲之建立一個自定義 Hook。)

9. 在 useEffect 中調用用函數時,要把該函數在 useEffect 中申明,不能放到外部申明,而後再在 useEffect 中調用

react.docschina.org/docs/hooks-…

function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }

  useEffect(() => {
    doSomething();
  }, []); // 🔴 這樣不安全(它調用的 `doSomething` 函數使用了 `someProp`)
}
複製代碼

要記住 effect 外部的函數使用了哪些 props 和 state 很難。這也是爲何 一般你會想要在 effect 內部 去聲明它所須要的函數。 這樣就能容易的看出那個 effect 依賴了組件做用域中的哪些值:

function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);
    }

    doSomething();
  }, [someProp]); // ✅ 安全(咱們的 effect 僅用到了 `someProp`)
}
複製代碼

只有 當函數(以及它所調用的函數)不引用 props、state 以及由它們衍生而來的值時,你才能放心地把它們從依賴列表中省略。下面這個案例有一個 Bug:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  async function fetchProduct() {
    const response = await fetch('http://myapi/product' + productId); // 使用了 productId prop
    const json = await response.json();
    setProduct(json);
  }
  useEffect(() => {
    fetchProduct();
  }, []); // 🔴 這樣是無效的,由於 `fetchProduct` 使用了 `productId`
  // ...
}
複製代碼

推薦的修復方案是把那個函數移動到你的 effect 內部。這樣就能很容易的看出來你的 effect 使用了哪些 props 和 state,並確保它們都被聲明瞭:

function ProductPage({ productId }) {
  const [product, setProduct] = useState(null);
  useEffect(() => {
    // 把這個函數移動到 effect 內部後,咱們能夠清楚地看到它用到的值。
    async function fetchProduct() {
      const response = await fetch('http://myapi/product' + productId);
      const json = await response.json();
      setProduct(json);
    }
    fetchProduct();
  }, [productId]); // ✅ 有效,由於咱們的 effect 只用到了 productId
  // ...
}
複製代碼

10. 如何在 Hooks 中優雅的 Fetch Data

www.robinwieruch.de/react-hooks… codesandbox.io/s/jvvkoo8pq…

import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState({ hits: [] });
  // 注意 async 的位置
  // 這種寫法,雖然能夠運行,可是會發出警告
  // 每一個帶有 async 修飾的函數都返回一個隱含的 promise
  // 可是 useEffect 函數有要求:要麼返回清除反作用函數,要麼就不返回任何內容
  useEffect(async () => {
    const result = await axios(
      'https://hn.algolia.com/api/v1/search?query=redux',
    );
    setData(result.data);
  }, []);
  return (
    <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul>
  );
}
export default App;
複製代碼
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState({ hits: [] });
  useEffect(() => {
    // 更優雅的方式
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );
      setData(result.data);
    };
    fetchData();
  }, []);
  return (
    <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul>
  );
}
export default App;
複製代碼

11. 不要過分依賴 useMemo

  • useMemo 自己也有開銷。useMemo 會「記住」一些值,同時在後續 render 時,將依賴數組中的值取出來和上一次記錄的值進行比較,若是不相等纔會從新執行回調函數,不然直接返回「記住」的值。這個過程自己就會消耗必定的內存和計算資源。所以,過分使用 useMemo 可能會影響程序的性能。
  • 在使用 useMemo 前,應該先思考三個問題:
    • 傳遞給 useMemo 的函數開銷大不大? 有些計算開銷很大,咱們就須要「記住」它的返回值,避免每次 render 都去從新計算。若是你執行的操做開銷不大,那麼就不須要記住返回值。不然,使用 useMemo 自己的開銷就可能超太重新計算這個值的開銷。所以,對於一些簡單的 JS 運算來講,咱們不須要使用 useMemo 來「記住」它的返回值。
    • 返回的值是原始值嗎? 若是計算出來的是基本類型的值(stringbooleannullundefinednumbersymbol),那麼每次比較都是相等的,下游組件就不會從新渲染;若是計算出來的是複雜類型的值(objectarray),哪怕值不變,可是地址會發生變化,致使下游組件從新渲染。因此咱們也須要「記住」這個值。
    • 在編寫自定義 Hook 時,返回值必定要保持引用的一致性。 由於你沒法肯定外部要如何使用它的返回值。若是返回值被用作其餘 Hook 的依賴,而且每次 re-render 時引用不一致(當值相等的狀況),就可能會產生 bug。因此若是自定義 Hook 中暴露出來的值是 object、array、函數等,都應該使用 useMemo 。以確保當值相同時,引用不發生變化。

12. useEffect 不能接收 async 做爲回調函數

useEffect 接收的函數,要麼返回一個能清除反作用的函數,要麼就不返回任何內容。而 async 返回的是 promise。 www.robinwieruch.de/react-hooks…

image.png

12、項目實戰

React Hooks 項目

十3、參考

官方提供的問題列表

React 使用規則

React Hooks 你真的用對了嗎?

大量自定義 hooks 的倉庫

從 Preact 中瞭解 React 組件和 hooks 基本原理表

2019年了,整理了N個實用案例幫你快速遷移到React Hooks

相關文章
相關標籤/搜索