1. 程式人生 > 實用技巧 >React-Hooks

React-Hooks

一、react-Hooks要解決什麼?

以下是上一代標準寫法類元件的缺點,也正是hook要解決的問題

  • 大型元件很難拆分和重構,也很難測試。
  • 業務邏輯分散在元件的各個方法之中,導致重複邏輯或關聯邏輯。
  • 元件類引入了複雜的程式設計模式,比如 Render props 和高階元件

設計目的

  • 加強版函式元件,完全不使用"類",就能寫出一個全功能的元件
  • 元件儘量寫成純函式,如果需要外部功能和副作用,就用鉤子把外部程式碼"鉤"進來

二、如何用好react-Hooks?

明確幾點概念

  • 所有的hook,在預設沒有依賴項陣列每次渲染都會更新
  • 每次 Render 時Props、State、事件處理、Effect等hooks都遵循 Capture Value 的特性
  • Render時會註冊各種變數,函式包括hooks,N次Render就會有N個互相隔離的狀態作用域
  • 如果你的useEffect依賴陣列為[],那麼它初始化一次,且使用的state,props等永遠是他初始化時那一次Render儲存下來的值
  • React 會確保 setState,dispatch,context 函式的標識是穩定的,可以安全地從 hooks 的依賴列表中省略

Function Component中每次Render都會形成一個快照並保留下來,這樣就確保了狀態可控,hook預設每次都更新,會導致重複請求等一系列問題,如果給[]就會一塵不變,因此用好hooks最重要就是學會控制它的變化

三、一句話概括Hook API

  • useState 非同步設定更新state
  • useEffect 處理副作用(請求,事件監聽,操作DOM等)
  • useContext 接收一個 context 物件並返回該 context 的當前值
  • useReducer 同步處理複雜state,減少了對深層傳遞迴調的依賴
  • useCallback 返回一個 memoized 回撥函式,避免非必要渲染
  • useMemo 返回一個 memoized 值,使得控制具體子節點何時更新變得更容易,減少了對純元件的需要,可替代shouldComponentUpdate
  • useRef 返回一個在元件的整個生命週期內保持不變 ref 物件,其 .current 屬性是可變的,可以繞過 Capture Value 特性
  • useLayoutEffect 其函式簽名與 useEffect 相同,但它會在所有的 DOM 變更之後同步呼叫 effect
  • useImperativeHandle 應當與 forwardRef 一起使用,將 ref 自定義暴露給父元件的例項值
  • useDebugValue 可用於在 React 開發者工具中顯示自定義 hook 的標籤

四、關注異同點

useState 與 this.setState

  • 相同點:都是非同步的,例如在 onClick 事件中,呼叫兩次 setState,資料只改變一次。
  • 不同點:類中的 setState 是合併,而useState中的 setState 是替換。

useState 與 useReducer

  • 相同點:都是操作state
  • 不同點:使用 useState 獲取的 setState 方法更新資料時是非同步的;而使用 useReducer 獲取的 dispatch 方法更新資料是同步的。
  • 推薦:當 state 狀態值結構比較複雜時,使用useReducer

useLayoutEffect 與 useEffect

  • 相同點:都是在瀏覽器完成佈局與繪製之後執行副作用操作
  • 不同點:useEffect 會延遲呼叫,useLayoutEffect 會同步呼叫阻塞視覺更新,可以使用它來讀取 DOM 佈局並同步觸發重渲染
  • 推薦:一開始先用 useEffect,只有當它出問題的時候再嘗試使用 useLayoutEffect

useCallback 與 useMemo

  • 相同點:都是返回memoized,useCallback( fn, deps) 相當於 useMemo( ( ) => fn, deps)
  • 不同點:useMemo返回快取的變數,useCallback返回快取的函式
  • 推薦:不要過早的效能優化,搭配食用口味更佳(詳見下文效能優化)

五、效能優化

在大部分情況下我們只要遵循 React 的預設行為,因為 React 只更新改變了的 DOM 節點,不過重新渲染仍然花費了一些時間,除非它已經慢到讓人注意了

react中效能的優化點在於:

  • 1、呼叫setState,就會觸發元件的重新渲染,無論前後的state是否不同
  • 2、父元件更新,子元件也會自動的更新

之前的解決方案

基於上面的兩點,我們通常的解決方案是:

  • 使用immutable進行比較,在不相等的時候呼叫setState;
  • 在 shouldComponentUpdate 中判斷前後的 props和 state,如果沒有變化,則返回false來阻止更新。
  • 使用 React.PureComponent

使用hooks function之後的解決方案

傳統上認為,在 React 中使用行內函數對效能的影響,與每次渲染都傳遞新的回撥會如何破壞子元件的 shouldComponentUpdate 優化有關, 使用useCallback快取函式引用,再傳遞給經過優化的並使用引用相等性去避免非必要渲染的子元件時,它將非常有用

  • 1、使用 React.memo等效於 PureComponent,但它只比較 props,且返回值相反,true才會跳過更新
const Button = React.memo((props) => {
  // 你的元件
}, fn);// 也可以自定義比較函式
  • 2、用 useMemo 優化每一個具體的子節點(詳見實踐3)
  • 3、useCallback Hook 允許你在重新渲染之間保持對相同的回撥引用以使得 shouldComponentUpdate 繼續工作(詳見實踐3)
  • 4、useReducer Hook 減少了對深層傳遞迴調的依賴(詳見實踐2)

如何惰性建立昂貴的物件?

  • 當建立初始 state 很昂貴時,我們可以傳一個 函式 給 useState 避免重新建立被忽略的初始 state
function Table(props) {
  // ⚠️ createRows() 每次渲染都會被呼叫
  const [rows, setRows] = useState(createRows(props.count));
  // ...
  // ✅ createRows() 只會被呼叫一次
  const [rows, setRows] = useState(() => createRows(props.count));
  // ...
}
  • 避免重新建立 useRef() 的初始值,確保某些命令式的 class 例項只被建立一次:
function Image(props) {
  // ⚠️ IntersectionObserver 在每次渲染都會被建立
  const ref = useRef(new IntersectionObserver(onIntersect));
  // ...
}
function Image(props) {
  const ref = useRef(null);
  // ✅ IntersectionObserver 只會被惰性建立一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }
  // 當你需要時,呼叫 getObserver()
  // ...
}

六、注意事項

Hook 規則

  • 在最頂層使用 Hook
  • 只在 React 函式中呼叫 Hook,不要在普通的JavaScript函式中呼叫
  • 將條件判斷放置在 hook 內部
  • 所有 Hooks 必須使用 use 開頭,這是一種約定,便於使用 ESLint 外掛 來強制 Hook 規範 以避免 Bug;
useEffect(function persistForm() {
  if (name !== '') {
    localStorage.setItem('formData', name);
  }
});

告訴 React 用到了哪些外部變數,如何對比依賴

useEffect(() => {
  document.title = "Hello, " + name;
}, [name]); // 以useEffect為示例,適用於所有hook

直到 name 改變時的 Rerender,useEffect 才會再次執行,保證了效能且狀態可控

不要在hook內部set依賴變數,否則你的程式碼就像旋轉的菊花一樣停不下來

useEffect(() => {
  const id = setInterval(() => {
    setCount(count + 1);
  }, 1000);
  return () => clearInterval(id);
}, [count]);// 以useEffect為示例,適用於所有hook

不要在useMemo內部執行與渲染無關的操作

  • useMemo返回一個 memoized 值,把“建立”函式和依賴項陣列作為引數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值,避免在每次渲染時都進行高開銷的計算。
  • 傳入 useMemo 的函式會在渲染期間執行,請不要在這個函式內部執行與渲染無關的操作。
constmemoizedValue = useMemo(()=>computeExpensiveValue(a, b), [a, b]);

七、實踐場景示例

實際應用場景往往不是一個hook能搞定的,長篇大論未必說的清楚,直接上例子(來源於官網摘抄,網路收集,自我總結)

1、只想執行一次的 Effect 裡需要依賴外部變數

【將更新與動作解耦】-【useEffect,useReducer,useState】

  • 1-1、使用setState的函式式更新解決依賴一個變數

該函式將接收先前的 state,並返回一個更新後的值

useEffect(() => {
  const id = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(id);
}, []);
  • 1-2、使用useReducer解決依賴多個變數
import React, { useReducer, useEffect } from "react";

const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { count: count + step, step };
  } else if (action.type === 'step') {
    return { count, step: action.step };
  } else {
    throw new Error();
  }
}
export default function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const { count, step } = state;
  console.log(count);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </>
  );
}

2、大型的元件樹中深層傳遞迴調

【通過 context 往下傳一個 dispatch 函式】-【createContext,useReducer,useContext】

/**index.js**/
import React, { useReducer } from "react";
import Count from './Count'
export const StoreDispatch = React.createContext(null);
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  switch (action.type) {
    case 'tick':
      return { count: count + step, step };
    case 'step':
      return { count, step: action.step };
    default:
      throw new Error();
  }
}
export default function Counter() {
  // 提示:`dispatch` 不會在重新渲染之間變化
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <StoreDispatch.Provider value={dispatch}>
      <Count state={state} />
    </StoreDispatch.Provider>
  );
}

/**Count.js**/
import React, { useEffect,useContext }  from 'react';
import {StoreDispatch} from '../index'
import styles from './index.css';

export default function(props) {
  const { count, step } = props.state;
  const dispatch = useContext(StoreDispatch);
  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  return (
    <div className={styles.normal}>
      <h1>{count}</h1>
      <input value={step} onChange={e => {
        dispatch({
          type: 'step',
          step: Number(e.target.value)
        });
      }} />
    </div>
  );
}

3、程式碼內聚,更新可控

【層層依賴,各自管理】-【useEffect,useCallback,useContext】

function App() {
  const [count, setCount] = useState(1);
  const countRef = useRef();// 在元件生命週期內保持唯一例項,可穿透閉包傳值

  useEffect(() => {
    countRef.current = count; // 將 count 寫入到 ref
  });
  // 只有countRef變化時,才會重新建立函式
  const callback = useCallback(() => {
    const currentCount = countRef.current //保持最新的值
    console.log(currentCount);
  }, [countRef]);
  return (
    <Parent callback={callback} count={count}/>
  )
}
function Parent({ count, callback }) {
  // count變化才會重新渲染
  const child1 = useMemo(() => <Child1 count={count} />, [count]);
  // callback變化才會重新渲染,count變化不會 Rerender
  const child2 = useMemo(() => <Child2 callback={callback} />, [callback]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

PPT模板下載大全https://www.wode007.com

八、自定義 HOOK

獲取上一輪的 props 或 state

function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

只在更新時執行 effect

function useUpdate(fn) {
  const mounting = useRef(true);
  useEffect(() => {
    if (mounting.current) {
      mounting.current = false;
    } else {
      fn();
    }
  });
}

元件是否銷燬

function useIsMounted(fn) {
  const [isMount, setIsMount] = useState(false);
  useEffect(() => {
    if (!isMount) {
      setIsMount(true);
    }
    return () => setIsMount(false);
  }, []);
  return isMount;
}

惰性初始化useRef

function useInertRef(obj) { // 傳入一個例項 new IntersectionObserver(onIntersect)
  const ref = useRef(null);
  if (ref.current === null) {
  // ✅ IntersectionObserver 只會被惰性建立一次
    ref.current = obj;
  }
  return ref.current;
}