總結react hooks

前言

文章雖然比較長,但是可以說是全網最全最有用的總結了,學會的記得分享、點贊、收藏、謝謝支持

React 在 v16.8 的版本中推出了 React Hooks 新特性。在我看來,使用 React Hooks 相比于從前的類組件有以下幾點好處:

代碼可讀性更強,原本同一塊功能的代碼邏輯被拆分在了不同的生命周期函數中,容易使開發者不利于維護和迭代,通過 React Hooks 可以將功能代碼聚合,方便閱讀維護;

組件樹層級變淺,在原本的代碼中,我們經常使用 HOC/render props 等方式來復用組件的狀態,增強功能等,無疑增加了組件樹層數及渲染,而在 React Hooks 中,這些功能都可以通過強大的自定義的 Hooks 來實現;

關于這方面的文章,我們根據使用場景分別進行舉例說明,幫助你認識理解并可以熟練運用 React Hooks 大部分特性。辛苦整理良久,還望手動點贊鼓勵~

一、State Hook

1、基礎用法

function State(){
  const [count, setCount] = useState(0);
  return (
      <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
              Click me
          </button>
      </div>
  )
}

2、更新

更新分為以下兩種方式,即直接更新和函數式更新,其應用場景的區分點在于:

直接更新不依賴于舊 state 的值;函數式更新依賴于舊 state 的值;

// 直接更新
setState(newCount);

// 函數式更新
setState(prevCount => prevCount - 1);

3、實現合并

與 class 組件中的 setState 方法不同,useState 不會自動合并更新對象,而是直接替換它。我們可以用函數式的 setState 結合展開運算符來達到合并更新對象的效果。

setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

4、惰性初始化 state

initialState 參數只會在組件的初始渲染中起作用,后續渲染時會被忽略。其應用場景在于:創建初始 state 很昂貴時,例如需要通過復雜計算獲得;那么則可以傳入一個函數,在函數中計算并返回初始的 state,此函數只在初始渲染時被調用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

5、一些重點

(1)不像 class 中的 this.setState ,Hook 更新 state 變量總是替換它而不是合并它;
(2)推薦使用多個 state 變量,而不是單個 state 變量,因為 state 的替換邏輯而不是合并邏輯,并且利于后續的相關 state 邏輯抽離;
(3)調用 State Hook 的更新函數并傳入當前的 state 時,React 將跳過子組件的渲染及 effect 的執行。(React 使用 Object.is 比較算法 來比較 state。)

二、Effect Hook

1、基礎用法

function Effect(){
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log(`You clicked ${count} times`);
  });

  return (
      <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
              Click me
          </button>
      </div>
  )
}

2、清除操作

為防止內存泄漏,清除函數會在組件卸載前執行;如果組件多次渲染(通常如此),則在執行下一個 effect 之前,上一個 effect 就已被清除,即先執行上一個 effect 中 return 的函數,然后再執行本 effect 中非 return 的函數。

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除訂閱
    subscription.unsubscribe();
  };
});

3、執行時期

與 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 調度的 effect 不會阻塞瀏覽器更新屏幕,這讓你的應用看起來響應更快;(componentDidMount 或 componentDidUpdate 會阻塞瀏覽器更新屏幕)

4、性能優化

默認情況下,React 會每次等待瀏覽器完成畫面渲染之后延遲調用 effect;但是如果某些特定值在兩次重渲染之間沒有發生變化,你可以通知 React 跳過對 effect 的調用,只要傳遞數組作為 useEffect 的第二個可選參數即可:如下所示,如果 count 值兩次渲染之間沒有發生變化,那么第二次渲染后就會跳過 effect 的調用;

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 僅在 count 更改時更新

5、模擬 componentDidMount

如果想只運行一次的 effect(僅在組件掛載和卸載時執行),可以傳遞一個空數組([ ])作為第二個參數,如下所示,原理跟第 4 點性能優化講述的一樣;

useEffect(() => {
  .....
}, []);

6、最佳實踐

要記住 effect 外部的函數使用了哪些 props 和 state 很難,這也是為什么 通常你會想要在 effect 內部 去聲明它所需要的函數。

// bad,不推薦
function Example({ someProp }) {
  function doSomething() {
    console.log(someProp);
  }

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

// good,推薦
function Example({ someProp }) {
  useEffect(() => {
    function doSomething() {
      console.log(someProp);
    }

    doSomething();
  }, [someProp]); // ? 安全(我們的 effect 僅用到了 `someProp`)
}

如果處于某些原因你無法把一個函數移動到 effect 內部,還有一些其他辦法:

你可以嘗試把那個函數移動到你的組件之外。那樣一來,這個函數就肯定不會依賴任何 props 或 state,并且也不用出現在依賴列表中了;萬不得已的情況下,你可以 把函數加入 effect 的依賴但 把它的定義包裹 進 useCallback Hook。這就確保了它不隨渲染而改變,除非它自身的依賴發生了改變;

推薦啟用 eslint-plugin-react-hooks 中的 exhaustive-deps 規則,此規則會在添加錯誤依賴時發出警告并給出修復建議 ;

// 1、安裝插件

npm i eslint-plugin-react-hooks --save-dev

// 2、eslint 配置

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

7、一些重點

(1)可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate和 componentWillUnmount這三個函數的組合;
(2)在 React 的 class 組件中,render 函數是不應該有任何副作用的;一般來說,在這里執行操作太早了,我們基本上都希望在 React 更新 DOM 之后才執行我們的操作。

三、useContext

用來處理多層級傳遞數據的方式,在以前組件樹中,跨層級祖先組件想要給孫子組件傳遞數據的時候,除了一層層 props 往下透傳之外,我們還可以使用 React Context API 來幫我們做這件事。使用例子如下所示

(1)使用 React Context API,在組件外部建立一個 Context

import React from 'react';
const ThemeContext = React.createContext(0);
export default ThemeContext;

(2)使用 Context.Provider提供了一個 Context 對象,這個對象可以被子組件共享

import React, { useState } from 'react';
import ThemeContext from './ThemeContext';
import ContextComponent1 from './ContextComponent1';

function ContextPage () {
  const [count, setCount] = useState(1);
  return (
    <div className="App">
      <ThemeContext.Provider value={count}>
        <ContextComponent1 />
      </ThemeContext.Provider>
      <button onClick={() => setCount(count + 1)}>
              Click me
      </button>
    </div>
  );
}

export default ContextPage;

(3)useContext()鉤子函數用來引入 Context 對象,并且獲取到它的值 // 子組件,在子組件中使用孫組件

import React from 'react';
import ContextComponent2 from './ContextComponent2';
function ContextComponent () {
  return (
    <ContextComponent2 />
  );
}
export default ContextComponent;
// 孫組件,在孫組件中使用 Context 對象值

import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ContextComponent () {
  const value = useContext(ThemeContext);
  return (
    <div>useContext:{value}</div>
  );
}
export default ContextComponent;

四、useReducer

1、基礎用法

比 useState 更適用的場景:例如 state 邏輯處理較復雜且包含多個子值,或者下一個 state 依賴于之前的 state 等;例子如下所示

import React, { useReducer } from 'react';
interface stateType {
  count: number
}
interface actionType {
  type: string
}
const initialState = { count: 0 };
const reducer = (state:stateType, action:actionType) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
};
const UseReducer = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div className="App">
      <div>useReducer Count:{state.count}</div>
      <button onClick={() => { dispatch({ type: 'decrement' }); }}>useReducer 減少</button>
      <button onClick={() => { dispatch({ type: 'increment' }); }}>useReducer 增加</button>
    </div>
  );
};

export default UseReducer;

2、惰性初始化 state

interface stateType {
  count: number
}
interface actionType {
  type: string,
  paylod?: number
}
const initCount =0 
const init = (initCount:number)=>{
  return {count:initCount}
}
const reducer = (state:stateType, action:actionType)=>{
  switch(action.type){
    case 'increment':
      return {count: state.count + 1}
    case 'decrement':
      return {count: state.count - 1}
    case 'reset':
      return init(action.paylod || 0)
    default:
      throw new Error();
  }
}
const UseReducer = () => {
  const [state, dispatch] = useReducer(reducer,initCount,init)

  return (
    <div className="App">
      <div>useReducer Count:{state.count}</div>
      <button onClick={()=>{dispatch({type:'decrement'})}}>useReducer 減少</button>
      <button onClick={()=>{dispatch({type:'increment'})}}>useReducer 增加</button>
      <button onClick={()=>{dispatch({type:'reset',paylod:10 })}}>useReducer 增加</button>
    </div>
  );
}
export default UseReducer;

五、Memo

如下所示,當父組件重新渲染時,子組件也會重新渲染,即使子組件的 props 和 state 都沒有改變

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

// 子組件
const ChildComp = () => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

// 父組件
const Parent = () => {
  const [count, setCount] = useState(0);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <div onClick={() => { setCount(count => count + 1); }}>點擊增加</div>
      <ChildComp/>
    </div>
  );
};

export default Parent;

改進:我們可以使用 memo 包一層,就能解決上面的問題;但是僅僅解決父組件沒有傳參給子組件的情況以及父組件傳簡單類型的參數給子組件的情況(例如 string、number、boolean等);如果有傳復雜屬性應該使用 useCallback(回調事件)或者 useMemo(復雜屬性)

// 子組件
const ChildComp = () => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

六、useMemo

假設以下場景,父組件在調用子組件時傳遞 info 對象屬性,點擊父組件按鈕時,發現控制臺會打印出子組件被渲染的信息。

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

// 子組件
const ChildComp = (info:{info:{name: string, age: number}}) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父組件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = { name, age };

  return (
    <div className="App">
      <div>hello world {count}</div>
      <div onClick={() => { setCount(count => count + 1); }}>點擊增加</div>
      <MemoChildComp info={info}/>
    </div>
  );
};

export default Parent;

分析原因:

點擊父組件按鈕,觸發父組件重新渲染;父組件渲染,const info = { name, age } 一行會重新生成一個新對象,導致傳遞給子組件的 info 屬性值變化,進而導致子組件重新渲染。

解決:

使用 useMemo 將對象屬性包一層,useMemo 有兩個參數:
第一個參數是個函數,返回的對象指向同一個引用,不會創建新對象;
第二個參數是個數組,只有數組中的變量改變時,第一個參數的函數才會返回一個新的對象。

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

// 子組件
const ChildComp = (info:{info:{name: string, age: number}}) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父組件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  
  // 使用 useMemo 將對象屬性包一層
  const info = useMemo(() => ({ name, age }), [name, age]);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <div onClick={() => { setCount(count => count + 1); }}>點擊增加</div>
      <MemoChildComp info={info}/>
    </div>
  );
};

export default Parent;

七 、useCallback

接著第六章節的例子,假設需要將事件傳給子組件,如下所示,當點擊父組件按鈕時,發現控制臺會打印出子組件被渲染的信息,說明子組件又被重新渲染了。

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

// 子組件
const ChildComp = (props:any) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父組件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = useMemo(() => ({ name, age }), [name, age]);
  const changeName = () => {
    console.log('輸出名稱...');
  };

  return (
    <div className="App">
      <div>hello world {count}</div>
      <div onClick={() => { setCount(count => count + 1); }}>點擊增加</div>
      <MemoChildComp info={info} changeName={changeName}/>
    </div>
  );
};

export default Parent;

分析下原因:

點擊父組件按鈕,改變了父組件中 count 變量值(父組件的 state 值),進而導致父組件重新渲染;父組件重新渲染時,會重新創建 changeName 函數,即傳給子組件的 changeName 屬性發生了變化,導致子組件渲染;

解決:

修改父組件的 changeName 方法,用 useCallback 鉤子函數包裹一層, useCallback 參數與 useMemo 類似

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

// 子組件
const ChildComp = (props:any) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父組件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = useMemo(() => ({ name, age }), [name, age]);
  const changeName = useCallback(() => {
    console.log('輸出名稱...');
  }, []);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <div onClick={() => { setCount(count => count + 1); }}>點擊增加</div>
      <MemoChildComp info={info} changeName={changeName}/>
    </div>
  );
};

export default Parent;

八、useRef

以下分別介紹 useRef 的兩個使用場景:

1、指向 dom 元素

如下所示,使用 useRef 創建的變量指向一個 input 元素,并在頁面渲染后使 input 聚焦

import React, { useRef, useEffect } from 'react';
const Page1 = () => {
  const myRef = useRef<HTMLInputElement>(null);
  useEffect(() => {
    myRef?.current?.focus();
  });
  return (
    <div>
      <span>UseRef:</span>
      <input ref={myRef} type="text"/>
    </div>
  );
};

export default Page1;

2、存放變量

useRef 在 react hook 中的作用, 正如官網說的, 它像一個變量, 類似于 this , 它就像一個盒子, 你可以存放任何東西. createRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用,如下例子所示:

import React, { useRef, useEffect, useState } from 'react';
const Page1 = () => {
    const myRef2 = useRef(0);
    const [count, setCount] = useState(0)
    useEffect(()=>{
      myRef2.current = count;
    });
    function handleClick(){
      setTimeout(()=>{
        console.log(count); // 3
        console.log(myRef2.current); // 6
      },3000)
    }
    return (
    <div>
      <div onClick={()=> setCount(count+1)}>點擊count</div>
      <div onClick={()=> handleClick()}>查看</div>
    </div>
    );
}

export default Page1;

九、useImperativeHandle

使用場景:通過 ref 獲取到的是整個 dom 節點,通過 useImperativeHandle 可以控制只暴露一部分方法和屬性,而不是整個 dom 節點。

十、useLayoutEffect

其函數簽名與 useEffect 相同,但它會在所有的 DOM 變更之后同步調用 effect,這里不再舉例。

useLayoutEffect 和平常寫的 Class 組件的 componentDidMount 和 componentDidUpdate 同時執行;

useEffect 會在本次更新完成后,也就是第 1 點的方法執行完成后,再開啟一次任務調度,在下次任務調度中執行 useEffect;

總結

關于這方面的文章,我們根據使用場景分別進行舉例說明,希望有幫助到你認識理解并可以熟練運用 React Hooks 大部分特性。

辛苦整理良久,還望手動點贊鼓勵~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372