React Hooks 基本使用詳解

Hooks let you use state and other React features without writing a class

Hooks 可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性

「一、類組件不足:」

二、「Hooks 優勢(優化類組件的三大問題):」

研發挑戰:

一開始的時候覺得 hooks 非常地神祕,寫慣了 class 式的組件後,我們的思維就會定格在那裏,生命週期,state,this 等的使用。因此會以 class 編寫的模式去寫函數式組件,導致我們一次又一次地爬坑,接下來我們就開始我們的實現方式講解。(提示:以下是都只是一種簡單的模擬方法,與實際有一些差別,但是核心思想是一致的)

三、Hooks 的 API

1. 使用 State Hook

演示地址:2.useState class 類 Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useClassState.js

import React, { Component } from "react";

class UseClassState extends Component {
  state = {
    count: 0
  };
  render() {
    const { count } = this.state;
    return (
      <button
        type="button"
        onClick={() => {
          this.setState({
            count: count + 1
          });
        }}
      >
        Click({count})
      </button>
    );
  }
}
export default UseClassState;

以上代碼很好理解,點擊按鈕讓 count 值加 1

接下來我們使用 useState 來實現上述功能。

栗子:1.useState Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useStateHook.js

import React, {useState} from 'react'

function App () {
  //返回兩個參數 一個是當前的狀態,還有一個是修改當前狀態的函數
  const [count, setCount] = useState(0)
  // 動態傳入參數 //延遲初始化,提高效率,只會在初始化時候執行一次
  // const [count,setCount] = useState(()=>{return props.defaultCount||0})
  
  onst [name, setName] = useState('小圓臉兒')
  return (
    <button type="button"
      onClick={() => {setCount(count + 1) }}
    >Click({count})</button>
  )
}

在這裏,useState 就是一個 Hook。通過在函數組件裏調用它來給組件添加一些內部 state,React 會在重複渲染時保留這個 state

useState 會返回一對值 **:當前狀態 ** 和一個讓你更新它的函數。你可以在事件處理函數中或其他一些地方調用這個函數。它類似 class 組件的 this.setState,但是它不會把新的 state 和舊的 state 進行合併。useState 唯一的參數就是初始 state

useState 讓代碼看起來簡潔了,但是我們可能會對組件中,直接調用 useState 返回的狀態會有些懵。既然 useState 沒有傳入任何的環境參數,它怎麼知道要返回的的是 count 的呢?而且還是這個組件的 count 不是其它組件的 count

初淺的理解: useState 確實不知道我們要返回的 count,但其實也不需要知道,它只要返回一個變量就行了。**「數組解構」**的語法讓我們在調用 useState 時可以給 state 變量取不同的名字。

useState 怎麼知道要返回當前組件的 state?

因爲 JavaScript 是單線程的。在 useState 被調用時,它只能在唯一一個組件的上下文中。

有人可能會問,如果一個組件內有多個 usreState,那 useState 怎麼知道哪一次調用返回哪一個 state 呢?

這個就是按照第一次運行的次序來順序來返回的。

接着上面的例子我們在聲明一個 useState:

...
const [count, setScount] = useState(0)
const [name, setName] = useState('小圓臉兒')
...

爲了防止我們使用 useState 不當,React 提供了一個 ESlint 插件幫助我們檢查。

「優化點」

通過上述我們知道 useState 有個默認值,因爲是默認值,所以在不同的渲染週期去傳入不同的值是沒有意義的,只有第一次傳入的纔有效。如下所示:

...
const defaultCount = props.defaultCount || 0
const [count, setCount] = useState(defaultCount)
...

state 的默認值是基於 props, 在 APP 組件每次渲染的時候 const defaultCount = props.defaultCount || 0 都會運行一次,如果它複雜度比較高的話,那麼浪費的資料肯定是可觀的。

const [count, setCount] = useState(() => {
  return props.defaultCount || 0
})

2. 使用 Effect Hooks

副作用的時機

現在使用 useEffect 就可以覆蓋上述的情況。

爲什麼一個 useEffect 就能涵蓋 Mount,Update,Unmount 等場景呢?

useEffect 標準上是在組件每次渲染之後調用,並且會根據自定義狀態來決定是否調用還是不調用。

第一次調用就相當於componentDidMount,後面的調用相當於 componentDidUpdateuseEffect 還可以返回另一個回調函數,這個函數的執行時機很重要。作用是清除上一次副作用遺留下來的狀態。

比如一個組件在第三次,第五次,第七次渲染後執行了 useEffect 邏輯,那麼回調函數就會在第四次,第六次和第八次渲染之前執行。嚴格來講,是在前一次的渲染視圖清除之前。如果 useEffect 是在第一次調用的,那麼它返回的回調函數就只會在組件卸載之前調用了,也就是componentWillUnmount

如果你熟悉 React class 的生命週期函數,你可以把 useEffect Hook 看做componentDidMountcomponentDidUpdatecomponentWillUnmount 這三個函數的組合。

在線舉栗子說明一下:4.class 類 useClassEffect Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useClassEffect.js

import React, { Component } from "react";
class UseClassEffect extends Component {
  state = {
    count: 0,
    size: {
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    }
  };
  onResize = () => {
    this.setState({
      size: {
        width: document.documentElement.clientWidth,
        height: document.documentElement.clientHeight
      }
    });
  };
  // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/resize_event
  componentDidMount() {
    document.title = this.state.count;
    window.addEventListener("resize", this.onResize, false);
  }
  componentWillMount() {
    window.removeEventListener("resize", this.onResize, false);
  }
  componentDidUpdate() {
    document.title = this.state.count;
  }
  render() {
    const { count, size } = this.state;
    return (
      <button
        type="button"
        onClick={() => {
          this.setState({ count: count + 1 });
        }}
      >
        Click({count}) size: {size.width}x{size.height}
      </button>
    );
  }
}
export default UseClassEffect;

對於生命週期我們在複習一遍:

上面主要做的就是網頁 title 顯示count 值,並監聽網頁大小的變化。這裏用到了componentDidMountcomponentDidUpdate 等副作用,因爲第一次掛載我們需要把初始值給 title, 當 count 變化時,把變化後的值給它 title,這樣 title 才能實時的更新。

「注意,我們需要在兩個生命週期函數中編寫重複的代碼。」

這邊我們容易出錯的地方就是在組件結束之後要**「記住銷燬事件的註冊」**,不然會導致資源的泄漏。現在我們把 App 組件的副作用用 useEffect 實現。

在線栗子 5.useEffect Demo:useEffectHook:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useEffectHook.js

import React, {useState, useEffect } from "react";
function UseEffectHook(props) {
  const [count, setCount] = useState(0);

  const [size, setSize] = useState({
    width: document.documentElement.clientWidth,
    height: document.documentElement.clientHeight
  });
  const onResize = () => {
    setSize({
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    });
  };
  // 第一個useEffect代表生命週期中的componentDidUpdate,
  useEffect(() => {
    document.title = count;
  }); // 不用第二個參數的情況執行幾次呢((不傳數組意味着每一次執行都會))
  // (只調用一次) 第二個useEffect代表生命週期中的componentDidMount組件更新 和 componentWillMount組件卸載
  useEffect(() => {
    window.addEventListener("resize", onResize, false);
    return () => {
      window.removeEventListener("resize", onResize, false);
    };
  }, []); //第二個參數纔是useEffect精髓,並且能優化性能,只有數組的每一項都不變的情況下,useEffect纔不會執行
  useEffect(() => {
    console.log("count:", count);
  }, [count]); //第二個參數我們傳入 [count], 表示只有 count 的變化時,我纔打印 count 值,resize 變化不會打印。

  return (
    <button
      type="button"
      onClick={() => {
        setCount(count + 1);
      }}
    >
      Click({count}) size: {size.width}x{size.height}
    </button>
  );
}
export default UseEffectHook;

對於上述代碼的第一個 useEffect, 相比類組件,Hooks 不在關心是 mount 還是 update。用 useEffect統一在渲染後調用,就完整追蹤了 count 的值。

對於第二個 useEffect, 我們可以通過返回一個回調函數來註銷事件的註冊。回調函數在視圖被銷燬之前觸發,銷燬的原因有兩種:「重新渲染和組件卸載」

這邊有個問題,既然 useEffect 每次渲染後都執行,難道我們每次都要綁定和解綁事件嗎?當然是完全不需要,只要使用 useEffect 第二個參數,並傳入一個空數組即可。第二個參數是一個可選的數組參數,只有數組的每一項都不變的情況下,useEffect 纔不會執行。第一次渲染之後, useEffect 肯定會執行。由於我們傳入的空數組,空數組與空數組是相同的,因此 useEffect 只會在第一次執行一次。

這也說明我們把 resize 相關的邏輯放在一起寫,不在像類組件那樣分散在兩個不同的生命週期內。同時我們處理 title 的邏輯與 resize 的邏輯分別在兩個 useEffect 內處理,實現關注點分離,不同的事情要分開放。

以上驗證了 hooks 組件相對於類組件編寫副作用邏輯類型有兩點:

「1. 提高了代碼複用」

「2. 實現了關注點分離」

 useEffect(() => {
    console.log('count:', count)
  }, [count])//第二個參數我們傳入 [count], 表示只有 count 的變化時,我纔打印 count 值,resize 變化不會打印。

第二個參數的三種形態,undefined, 空數組及非空數組,我們都經歷過了,但是咱們沒有看到過回調函數的執行。

我現在描述一種場景就是在組件中訪問 Dom 元素,在 Dom 元素上綁定事件,在上述的代碼中添加以下代碼:

...
 const onClick = () => {
  console.log('click');
 }
// 新增一個 DOM 元素,在新的 useEffect 中監聽 span 元素的點擊事件。
// useEffect(() => {
//   document.querySelector('#size').addEventListener('click', onClick, // // // false);
// },[])
  useEffect(() => {
    document.querySelector("#size").addEventListener("click", onClick, false);
    return () => {
      document
        .querySelector("#size")
        .removeEventListener("click", onClick, false);
    };
  }, []);
  return (
    <div>
    ...
   {/* 情況一::*/}   
   {/* <span>size: {size.width}x{size.height}</span>*/}
     {/* 情況二:假如我們 span 元素可以被銷燬重建,我們看看會發生什麼情況,改造一下代碼:*/}
     {
        count%2
        ? <span>我是span</span>
        : <p>我是p</p>
      }
      
  </div>
)

情況一打印結果:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useEffectHook.js

情況二打印結果:

可以看出一旦 dom 元素被替換,我們綁定的事件就失效了,所以咱們始終要追蹤這個 dom 元素的最新狀態。

使用 useEffect ,最合適的方式就是使用回調函數來處理了,同時要保證每次渲染後都要重新運行,所以不能給第二次參數設置 [],改造如下:

useEffect(() => {
 document.querySelector('#size').addEventListener('click', onClick, false);
   return () => {
     document.querySelector('#size').removeEventListener('click', onClick, false);
   }
})

情況三:

無論是之前的生命週期函數,還是 useEffect 都是處理副作用的,之前的生命週期函數在命名的時候比較容易理解,但其實都是圍繞着組件的渲染和重渲染的;useEffect 抽象了一層,通過第二個參數執行的時機與生命週期是等價的,大家需要理解什麼樣的 useEffect 參數與什麼樣的生命週期函數是對應的,差不多也就靈活運用 useEffect 了。當然參數化的 useEffect 肯定不止有這點能耐,只要你能精確控制第二個參數,就能節省運行時性能還能寫出可維護性很高的代碼,無論如何還是建議多練習。

問題:useEffect 的第二個參數只要在數組成員都不變的情況下才不會運行副作用,那麼如何理解這個不變呢?

3. 使用 Context Hooks

我們已經學了 useState 和 useEffect,他倆已經能滿足大部分的開發需求了,但是爲了開發效率和性能着想,我們接下來還要認識使用率沒那麼高,但是在函數組件的編寫中依然發揮着重要角色的幾個 hook 函數

useContext

這個 context 就是 React 高級用法中提到的 Context,複習一下 Context,Context 能夠允許數據跨越組件層級直接傳遞。

使用 Context , 首先頂層先聲明 Provier 組件,並聲明 value 屬性,接着在後代組件中聲明 Consumer 組件,這個 Consumer 子組件,只能是唯一的一個函數,函數參數即是 Context 的負載。如果有多個 Context , Provider 和 Consumer 任意的順序嵌套即可。

此外我們還可以針對任意一個 Context 使用 contextType 來簡化對這個 Context 負載的獲取。但在一個組件中,即使消費多個 Context, contextType 也只能指向其中一個。

在 Hooks 環境中,依舊可以使用 Consumer,但是 ContextType 作爲類靜態成員肯定是用不了。Hooks 提供了 useContext, 不但解決了 Consumer 難用的問題同時也解決了 contextType 只能使用一個 context 的問題。

最基本的 Context 使用栗子:6.Context Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/contextDemo.js

import React, { Component, createContext, useState,useContext } from "react";
const CountContext = createContext();
// 1.使用類形式的栗子:
class Foo extends Component {
  render() {
    return (
      <CountContext.Consumer>
        {(count) => <h1>基本 Context Demo:{count}</h1>}
      </CountContext.Consumer>
    );
  }
}
// 2.接着將 Foo 改成用 contextType 的形式:
class Bar extends Component {
  static contextType = CountContext;

  render() {
    const count = this.context;

    return <h1>使用contextType Context Demo:{count}</h1>;
  }
}
// 3.接着使用 useContext 形式:
// 在函數組件中,useContext可不是緊緊可以獲取一個Context,從語法上看對Context的數量完全沒有限制;解決了 Consumer 難用的問題同時也解決了 contextType 只能使用一個 context 的問題。
function Counter() {
  const count = useContext(CountContext);

  return <h1>在函數組件中,useContext demo:{count}</h1>;
}
function ContextDemo(props) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        點擊({count})
      </button>
      <CountContext.Provider value={count}>
        <Foo />
        <Bar />
        <Counter/>
      </CountContext.Provider>
    </div>
  );
}
export default ContextDemo;

useContext 接收一個 context 對象(React.createContext 的返回值)並返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的 <CountContext.Provider> 的 value prop 決定。

當組件上層最近的 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 CountContext provider 的 context value 值。

別忘記 useContext 的參數必須是 context 對象本身:

4. 使用 Memo /CallBack Hooks

useCallback 是 useMemo 的變種。

回顧:meno 用來優化函數組件重渲染的行爲,當傳入屬性值都不變的情況下,就不會觸發組件的重渲染,否則就會觸發組件重渲染。

useMemo 與 memo

image-20210913013342998

memo針對的是一個組件的渲染是否重複執行,而 useMemo 定義的是一段函數邏輯是否重複執行,本質都是利用同樣的算法來判定依賴是否發生改變,進而決定是否觸發特定邏輯。有很多這樣的邏輯輸入輸出是對等的,相同的輸入一定產生相同的輸出,數學上稱之爲冪等,useMemo 就可以減少重複的重複計算減少資源浪費。所以嚴格來講不適用 memo 或者 useMemo 不應該會導致你的業務邏輯發生變化,換句話說 memo 和 useMemo 僅僅用來做性能優化使用。

memo 和 useMemo 僅僅用來性能優化使用。

舉個栗子:7.useMemo Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook.js

import React, { useState, useMemo } from "react";
function Foo(props) {
  return <h1>{props.count}</h1>;
}

function UseMemoHook(props) {
  const [count, setCount] = useState(0);

  const double = useMemo(() => {
    return count * 2;
  }, [count]);

  return (
    <div>
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click ({count}) double : ({double})
      </button>

      <Foo count={count} />
    </div>
  );
}
export default UseMemoHook;

如上所示, useMemo 語法與 useEffect 是一致的。第一個參數是需要執行的邏輯函數,第二個參數是這個邏輯依賴輸入變量組成的數組,如果不傳第二個參數,這 useMemo 的邏輯每次就會運行, useMemo 本身的意義就不存在了,所以需要傳入參數。所以傳入空數組就只會運行一次,策略與 useEffect 是一樣的,但有一點比較大的差異就是調用時機, useEffect 執行的是副作用,所以一定是渲染之後才執行,但 useMemo 是需要返回值的,而返回值可以直接參與渲染,因此 useMemo 是在渲染期間完成的, 有這樣一前一後的區別。

稍微改動一下,count===3,則參數就變成了布爾值 Boolean

const double = useMemo(() => {
    return count * 2;
  }, [count === 3]);

https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook.js

現在能斷定, count 在等於 3 之前,由於這個條件一直保存 false 不變,double 不會重新計算,所以一直是 0,當 count 等於 3, double 重新計算爲 6,當 count 大於 3, double 在重新計算,變成 8,然後就一直保存 8 不變。所以只要找到你真正依賴了哪些參數,就能儘可能的避免沒有必要的計算。

useMemo 也可以依賴另外一個 useMemo,比如:

//useMemo也可以依賴另外一個useMemo,比如:
  const half = useMemo(()=>{
    return double/4
  },[double])

但是一定不要**「循環依賴」**,會把瀏覽器搞崩潰的

記住, 傳入的 useMemo 的函數會在渲染期間執行,請不要在這個函數內部執行與渲染無關的聽任,諸如副作用這類操作屬於 useEffect 的適用範疇,而不是 useMemo

「你可以把 useMemo 作爲性能優化的手段,但不要把它當成語義上的保證。」

使用 Callback Hooks

先重現一個使用 memo 優化子組件重渲染的場景: 8.memo Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook2.js

import React, { useState, useMemo, memo } from "react";
const Counter = memo(function Counter(props) {
  console.log("counter");
  return <h1>{props.count}</h1>;
});

function UseMemoHook(props) {
  const [count, setCount] = useState(0);

  //稍微改動一下,count===3,則參數就變成了布爾值Boolean
  const double = useMemo(() => {
    return count * 2;
  }, [count === 3]);
  //useMemo也可以依賴另外一個useMemo,比如:
  const half = useMemo(() => {
    return double / 4;
  }, [double]);
  return (
    <div>
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click ({count}) double : ({double}) half:({half})
      </button>

      <Counter count={double} />
    </div>
  );
}
export default UseMemoHook;

使用 memo 包裹 Counter 組件,這樣只有當 double 變化時, Counter 組件纔會重新渲染,執行裏面的 log, 運行結果請看 8.memo Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useMemoHook2.js

現在在給 Counter 中的 h1 添加一個 click 事件:

const Counter = memo(function Counter(props) {
  console.log("counter");
  return <h1 onClick={props.onClick}>{props.count}</h1>;
});

然後在 UseMemoHook2 組件中聲明 onClick 並傳給 Counter組件:

function UseMemoHook2(props) {
  ...
  // 優化前
  const onClick=()=>{
    console.log('onClick')
  }
  return (
    <div>
      ...
      <Counter count={double} onClick={onClick} />
    </div>
  );
}
export default UseMemoHook2;

結果請看 8.memo Demo 優化前片段

可以看出,每次點擊,不管 Counter 是否有變化, Counter 組件都會被渲染。那就說明每次 UseMemoHook2 重新渲染之後, onClick 句柄的變化,導致 Counter 也被連帶重新渲染了。count 經常變化可以理解,但是 onClick就不應該經常變化了,畢竟只是一個函數而已,所以我們要想辦法讓 onClick 句柄不變化。

想想我們上面講的 useMemo,可以這樣來優化 onClick:

// 優化後
   const onClick = useMemo(() => {
     return ()=>{
       console.log("onClick")
      } 
   },[])

由於我們傳給 useMemo 的第二個參數是一個空數組,那麼整個邏輯就只會運行一次,理論上我們返回的 onClick 就只有一個句柄。

運行效果:請看 8.memo Demo 優化後片段

現在我們把 useCallback 來實現上頁 useMemo 的邏輯。

// 優化後
   const onClick = useCallback(() ={
       console.log("onClick")
   },[])

如果 useMemo 返回的是一個函數,那麼可以直接使用 useCallback 來省略頂層的函數。

大家可能有一個疑問, useCallback 這幾行代碼明明每次組件渲染都會創建新的函數,它怎麼就優化性能了呢?

注意,大家不要誤會,使用 useCallback 確實不能阻止創建新的函數,但這個函數不一定會被返回,換句話說很可能這個新創建的函數就被拋棄不用了。useCallback解決的是:傳入子組件的函數參數過度變化,導致子組件過度渲染的問題,這裏一定要理解好,不要對 useCallback 有誤解。

上述我們這個 onClick 函數什麼都不依賴,因此 useCallback 的第二個參數纔是空數組,也就是讓 useCallback 的邏輯只運行一次,在實際業務中不會這麼簡單,至少也要更新一下狀態,舉個栗子:

function useMemo(props) {
...
  const [clickCount, setClickCount] = useState(0);

  const onClick = useCallback(() => {
    console.log("Click");

    setClickCount(clickCount + 1);
  }, [clickCount, setClickCount]);//setClickCount不需要寫,因爲 React 能保證 setState 每次返回的都是同個句柄
  ...
}

在 UseMemoHook2 組件中在聲明一個 useState,然後在 onClick 中調用 setClickCount,此時 onClick 依賴 clickCount, setClickCount

其實這裏的 setClickCount 是不需要寫的,因爲 React 能保證 setState 每次返回的都是同個句柄。不信,可以看下官方文檔 :

注意

React 會確保 dispatch 函數的標識是穩定的,並且不會在組件重新渲染時改變。這就是爲什麼可以安全地從 useEffectuseCallback 的依賴列表中省略 dispatch

這裏的場景,除了直接使用 setClickCount+1 賦值以外, 還有一種方式甚至連 clickCount都不用依賴。setState 除了傳入對應的 state 最新值以外,還可以傳入一個函數,函數的參數即這個 state的當前值,返回就是要更新的值:

// 傳入一個函數,函數的參數即這個 `state`的當前值,返回就是要更新的值
  const onClick = useCallback(() => {
    console.log("Click");
    setClickCount((clickCount) => clickCount + 1);
  }, []);

useMemo、useCallback 小結

memo 根據屬性來決定是否重新渲染組件一樣, useMemo 可以根據指定的依賴來決定一段函數邏輯是否重新執行,從而優化性能。

如果 useMemo 的返回值是函數的話,那麼就可以簡寫成 useCallback 的方式,只是簡寫而已,實際並沒有區別。

需要特別注意的是,當依賴變化時,我們能斷定 useMemo 一定重新執行。但是注意,即使依賴不變化我們不能假定它就一定不會重新執行,也就是說,它也可能重新執行,就是考慮內存優化的結果。總之,useMemo 和 useCallback 是用來錦上添花的優化手段,不可以過度依賴他是否觸發重新渲染,因爲 React 沒有給我們打包票說一定重新執行或者一定不重新執行,useMemo 使用場景很多,特別是 useCallback 傳遞給 useCallback 子組件

我們可以把 useMemo, useCallback 當做一個錦上添花優化手段,不可以過度依賴它是否重新渲染,因爲 React 目前沒有打包票說一定執行或者一定不執行。

5. 使用 Ref Hooks

類組件中使用 Ref 一般有:

上述在函數組件中沒有辦法使用它們,取而代之的是 useRef Hooks。

useRef 主要有兩個使用場景:

大家可能會想到 state 也可跨越渲染週期保存,但是 state 的賦值會觸發重渲染,但是 ref 不會,從這點看 ref 更像是類屬性中的普通成員。

「舉例說明一下:使用 useRef 獲取子組件或者 DOM 節點的句柄」:9.useRef Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/useRefHook.js

import React, {
  PureComponent,
  useState,
  useMemo,
  useCallback,
  useRef
} from "react";
// const Counter = memo(function Counter(props) {
//   console.log("counter");
//   return <h1 onClick={props.onClick}>{props.count}</h1>;
// });
class Counter extends PureComponent {
  speak() {
    console.log(`now counter is:${this.props.count}`);
  }
  render() {
    const { props } = this;
    return <h1 onClick={props.onClick}>{props.count}</h1>;
  }
}

function UseRefHook(props) {
  const [count, setCount] = useState(0);
  // 依賴多個數據變化的時候
  const [clickCount, setClickCount] = useState(0);
  const counterRef = useRef();

  //稍微改動一下,count===3,則參數就變成了布爾值Boolean
  const double = useMemo(() => {
    return count * 2;
  }, [count === 3]);
  const onClick = useCallback(() => {
    console.log("Click");
    setClickCount((clickCount) => clickCount + 1);
    console.log(counterRef.current);
    counterRef.current.speak();
  }, [counterRef]);
  return (
    <div>
      <button
        type="button"
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Click ({count}) double : ({double})
      </button>
      <Counter ref={counterRef} count={double} onClick={onClick} />
    </div>
  );
}
export default UseRefHook;

下面我們來嘗試一下,不用 ref 來保存 DOM 或者組件,而是保存一個普通變量:

「粟例說明一下:同步渲染週期之間的共享數據的存儲」

假設我有這樣一個場景,組件一掛載就讓 count 狀態每秒鐘自動 + 1,當 count>=10 以後就不再自動增加,顯然這是一個副作用, 首先我們需要引入 useEffect,在 UseRefHook 中我們需要定義兩個副作用:第一個僅作用一次,用來啓動定時器;第二個始終作用用來判斷 count 的值是否滿足 >=10 的條件;

let it;
// 第一個ueeEffect:僅作用一次
useEffect(() => {
    it = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);
// 第二個ueeEffect始終作用用來判斷count的值是否滿足>=10的條件
  useEffect(() => {
    if (count >= 10) {
      clearInterval(it);
    }
  });

然而結果到 10 以後並沒有停止增加,爲什麼?顯然就是因爲在 clearInterval 的時候,定時器的句柄 it 這個變量已經不是 setInterval 的賦值了,每次 UseRefHook 重渲染都會重置他,那我們把 it 放在 state 中麼?用 useState 聲明能解決麼?但是 it 並沒有參與渲染,而且弄不好在副作用裏面更新或導致死循環,那麼現在 useRef 就派上用場了:

let it = useRef();
useEffect(() => {
    it.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);
  useEffect(() => {
    if (count >= 10) {
      clearInterval(it.current);
    }
  });

使用 useRef 來創建一個 it, 當 setInterval 返回的結果賦值給 itcurrent 屬性。

這回效果到 10 停止了更新,ref 幫我們解決了這個問題,是不是很像類屬性成員,如果遇見

需要訪問上一次渲染時候的數據,甚至是 state,就把他們同步到 ref 中,下一次渲染就能夠正確的獲取到了。

兩種 ref 的使用場景都講過了:

useRef 主要有兩個使用場景:

你應該熟悉 ref 這一種訪問 DOM 的主要方式。如果你將 ref 對象以 <divref={myRef}/> 形式傳入組件,則無論該節點如何改變,React 都會將 ref 對象的 .current 屬性設置爲相應的 DOM 節點。

然而, useRef()ref 屬性更有用 **。它可以很方便地保存任何可變值 **,其類似於在 class 中使用實例字段的方式。

這是因爲它創建的是一個普通 Javascript 對象。而 useRef() 和自建一個 {current:...} 對象的唯一區別是, useRef 會在每次渲染時返回同一個 ref 對象。

請記住,當 ref 對象內容發生變化時,useRef 並不會通知你。變更 .current 屬性不會引發組件重新渲染。如果想要在 React 綁定或解綁 DOM 節點的 ref 時運行某些代碼,則需要使用回調 ref 來實現。

在副作用裏面如何判定一個元素或者組件在本次渲染和上次渲染之間有過重新創建呢?

6. 自定義 Hooks

前面三篇,我們講到優化類組件的三大問題:

對於組件的複用狀態沒怎麼說明,現在使用自定義 Hook 來說明一下。

首先我們把上面的例子用到 count 的邏輯的用自定義 Hook 封裝起來:10. 自定義 Hook Demo:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/customHook.js

import React, { useState, useEffect, useRef } from "react";
function useCount(defaultCount) {
  const [count, setCount] = useState(0);
  let it = useRef();
  useEffect(() => {
    it.current = setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []);
  useEffect(() => {
    if (count >= 10) {
      clearInterval(it.current);
    }
  });

  return [count, setCount];
}

function CustomHook(props) {
  const [count, setCount] = useCount(0);
  return (
    <div>
      <h1>{count}</h1>
    </div>
  );
}
export default CustomHook;

可以看出運行效果跟上面是一樣的。

「定義 Hook 是一個函數,其名稱以 “use” 開頭,函數內部可以調用其他的 Hook」。我們在函數自定義寫法上似乎和編寫函數組件沒有區別,確實自定義組件與函數組件的最大區別就是**「輸入與輸出的區別」**。

再來一個特別的 Hook 加深一下映像。在上述代碼不變的條件下,我們在加一個自定義 Hook 內容如下:

function useCounter(count) {
  return <h1>{count}</h1>;
}
function CustomHook(props) {
  const [count, setCount] = useCount(0);
  const Counter = useCounter(count);
  return (
    <div>
      <h2>{Counter}</h2>
    </div>
  );
}

我們自定義 useCounter Hook 返回的是一個 JSX,運行效果是一樣的,所以 Hook 是可以返回 JSX 來參與渲染的,更說明 Hook 與函數組件的相似性。

7.Hooks 使用法則

只在最頂層使用 Hook

「不要在循環,條件或嵌套函數中調用 Hook,」 確保總是在你的 React 函數的最頂層以及任何 return 之前調用他們。遵守這條規則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調用。這讓 React 能夠在多次的 useStateuseEffect 調用之間保持 hook 狀態的正確。(如果你對此感到好奇,我們在下面會有更深入的解釋。)

只在 React 函數中調用 Hook

** 不要在普通的 JavaScript 函數中調用 Hook。** 你可以:

遵循此規則,確保組件的狀態邏輯在代碼中清晰可見。

8.Hooks 使用過程中注意常見問題

官方文章上說的很明白,此處不在贅述:https://zh-hans.reactjs.org/docs/hooks-faq.html

小結

本文主要是對 React Hooks 的基本使用的介紹,其中的 DEMO 鏈接如下:https://codesandbox.io/s/react-hooks-demo-odfnt?file=/src/index.js

所有 demo 都被引用在了一個 index.js 文件中,可根據文章介紹 demo 序號進行操作演示

查看本文不方便也可看掘金上已同步:https://juejin.cn/post/7008157923532767246/

參考

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/P06gh0xPbiDv7GQvQPJeEQ