React Hooks的學習筆記

蘇里發表於2019-03-23

"Unlearn what you have learned" -- Yoda

前言

有一天我在逛Medium的時候,突然發現了一篇介紹React Hooks的文章,我認真看了一遍後,計劃好好了解一下它。

在學習Hooks之前,官網上說了Hooks是完全可用的(v16.8.0),並沒有破壞性變更,而且完全向後相容,與其說是一種新API,不如說是React Team他們把React更核心的運算元據與UI的能力挖掘了出來。

嗯美滋滋~學完應該可以在工作專案裡用了!開始學習吧!

Hooks的起步使用

其實Hooks主要常用的可以有以下幾個:

  • useState
  • useEffect
  • useContext
  • useMemo
  • useRef
  • useReducer
  • useCallback

列舉的以上這幾個,其實已經算是比較常用的,尤其是前兩個,接下來就會介紹它們部分幾個的使用。

useState

useState這個鉤子其實對應的是我們之前class Component裡的this.setState

  1. useState傳參代表預設值,可以是原始值,也可以是物件、陣列,所以其實表達能力很豐富。
  2. useState呼叫後返回是一對值,對應當前的值更新這個值的函式,用陣列解構的方式獲取很簡潔。
  3. useState在一個函式元件裡可以多次使用。
  4. useStatethis.setState區別之處在於,前者每次更新後state都是新值,換而言之其實是不可變資料的概念。而後者使用後,其實更新state部分的值,引用本身並無改變。

簡單使用如下示例。

import React, { useState } from 'react';

export default function StateHook() {
  const [count, useCount] = useState(0);
  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={() => useCount(count + 1)}>Click me</button>
    </>
  );
}
複製程式碼

useEffect

useEffect這個鉤子勢必是我們常用的。

  1. 它基本可以等價於componentDidMountcomponentDidUpdate的這兩個生命週期鉤子組合的效果。那麼它的呼叫時機大概是每次渲染結束後,所以不會阻塞元件渲染。
  2. useEffect一般用於實現設定資料請求、監聽器等有副作用的功能,傳入的第一個引數函式A1用於設定副作用,而是傳入的這個函式可以返回一個函式A2用於取消函式A1的副作用。這兩個函式的React呼叫它們時機分別在於,註冊副作用的函式A1在當次渲染結束後立即執行,取消副作用的函式A2在下次渲染開始之前立即執行。再次強調,這麼設計的理由還是為了不阻塞元件渲染。
  3. useEffect第二個引數用於設定副作用的依賴陣列。什麼意思?思維靈活的同學已經想到了,如果每次渲染都執行副作用,有可能造成效能浪費,那麼可以通過告訴React,這個鉤子依賴某些props或者states,在這些依賴不發生改變時,這個副作用不會再重複執行。在以下的例子中,可以傳空陣列,告訴React該副作用什麼也不依賴,那麼它只會在第一次渲染時執行一次(但是一般不推薦這麼做)。如果不傳第二個引數,則意味著每次渲染都必然執行一次,此時應當注意記憶體洩露。
  4. 同學們有沒有發現,使用useEffect後,一個副作用的註冊監聽與對應的取消註冊邏輯全部放在了一起,對比與以往的分別在componentDidMountcomponentDidUpdatecomponentWillUnmount裡分散同一副作用的邏輯。useEffect的使用更有吸引力和說服力了。
import React, { useState, useEffect } from 'react';

export default function EffectHook({ dep }) {
  const [width, setWidth] = useState(window.innerWidth);
  
  function handleWindowResize() {
    const w = window.innerWidth;
    setWidth(w);
  }
  
  useEffect(() => {
    window.addEventListener('resize', handleWindowResize);
    return () => {
      window.removeEventListener('resize', handleWindowResize);
    };
  }, 
    // deps
    []
  );

  return (
    <>
      <p>window.innerWidth: {width}</p>
    </>
  );
}
複製程式碼

useContext

這個鉤子還是和原有的Context.ProviderContext.Consumer一樣的理解即可。用法示例如下,理解方便,不再贅述。

import React, { useContext } from 'react';

export const souliz = {
  name: 'souliz',
  description: 'A normal human named by his cat.'
};

export const UserContext = React.createContext(souliz);

export default function ContextHook() {
  const context = useContext(UserContext);

  return (
    <>
      <p>UserContext name: {context.name}</p>
      <p>UserContext description: {context.description}</p>
    </>
  );
}
複製程式碼

useMemo

有時候我們會遇到一個極耗效能的函式方法,但由於依賴了函式元件裡一些狀態值,又不得不放在其中。那麼如果我們每次渲染都去重複呼叫的發,元件的渲染必然會十分卡頓。

因此寫了以下示例驗證,一個計算斐波那契的函式(眾所周知的慢),讀者可以拷貝這段程式碼,註釋useMemo那一行,使用直接計算來看,點選按鈕觸發元件重新渲染,會發現很卡頓(當然了),那麼此時useMemo作用就發揮出來了,其實理解上還是和原有的React.memo一樣,可用於快取一下計算緩慢的函式,如果依賴沒有發生改變,則重複使用舊值。前提必然是這個函式是一個純函式,否則必然會引發問題。

useCallback其實也和useMemo道理類似,不過它解決的問題其實如果依賴不改變,使用舊的函式引用,在useEffect的依賴是函式時,可以使用useCallback的特性來避免重複觸發副作用的發生,因此不再贅述useCallback

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

let fib = n => (n > 1 ? fib(n - 1) + fib(n - 2) : n);
let renders = 0;

export default function MemoHook() {
  const defaultInput = 37;
  const [input, setInput] = useState(defaultInput);
  const [time, setTime] = useState(0);
  const value = useMemo(() => fib(input), [input]);
  // 來來來,看看不使用Memo的後果就是卡頓
  // const value = fib(input);

  return (
    <>
      <p>fib value is {value}</p>
      <input
        type="number"
        value={input}
        onChange={e => setInput(e.target.value)}
      />
      <button onClick={() => setTime(time + 1)}>Trigger render {time}</button>
      <footer>render times: {renders++}</footer>
    </>
  );
}
複製程式碼

useRef

useRef這個鉤子需要更通用的理解方式,不同於我們之前使用的React.createRef(),這個鉤子用於建立的是一個引用物件,那麼可以用於突破useState所帶來的侷限。什麼意思呢?useState每次渲染都是新的值,也就是下面示例中,如果我點選3次按鈕,分別更新了值觸發5次元件重新渲染,那麼通過延時5秒後獲取current值如示例二,如果需要在某些操作中獲取元件最新的某些state是最新的值的時候,useRef可以派上大用場。

import React, { useRef, useEffect, useState } from 'react';

export default function RefHook() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  latestCount.current = count;
  useEffect(() => {
    setTimeout(() => {
      console.log(`Ref: You clicked ${latestCount.current} times`);
      console.log(`state: You clicked ${count} times`);
    }, 5000);
  });

  return (
    <>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </>
  );
}
複製程式碼
Ref: You clicked 3 times
state: You clicked 1 times
Ref: You clicked 3 times
state: You clicked 2 times
Ref: You clicked 3 times
state: You clicked 3 times
複製程式碼

useReducer

相信同學們都使用過redux,React Team考慮到這種使用方式常見,於是設計出來了這麼一個鉤子。這樣的話其實解決了我們常見寫redux的多檔案跳躍編寫的煩惱,而且十分易於理解。(當然還有比較高階的用法)。以下程式碼示例。

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

const defaultTodos = [
  {
    id: 1,
    text: 'Todo 1',
    completed: false
  },
  {
    id: 2,
    text: 'Todo 2',
    completed: false
  }
];

function todosReducer(state, action) {
  switch (action.type) {
    case 'add':
      return [
        ...state,
        {
          id:  Date.now(),
          text: action.text,
          completed: false
        }
      ];
    case 'complete':
      return state.map(todo => {
        if (todo.id === action.id) {
          todo.completed = true;
        }
        return todo;
      });
    default:
      return state;
  }
}

export default function ReducerHook() {
  const [todos, dispatch] = useReducer(todosReducer, defaultTodos);
  const [value, setValue] = useState('');

  function handleTextChange(e) {
    setValue(e.target.value);
  }

  function handleAddTodo() {
    if (value === '') {
      return;
    }
    dispatch({
      type: 'add',
      text: value
    });
    setValue('');
  }

  function handleCompleteTodo(id) {
    dispatch({
      type: 'complete',
      id
    });
  }

  return (
    <>
      <section>
        <input
          type="text"
          onChange={handleTextChange}
          value={value}
        />
        <button onClick={handleAddTodo}>Add Todo</button>
      </section>
      <ul className="todos">
        {todos.map(todo => (
          <ol id={todo.id} key={todo.id}>
            <span
              style={{
                textDecoration: todo.completed ? 'line-through' : 'none'
              }}
            >
              {todo.text}
            </span>
            <input
              type="checkbox"
              disabled={todo.completed}
              onClick={() => handleCompleteTodo(todo.id)}
            />
          </ol>
        ))}
      </ul>
    </>
  );
}
複製程式碼

其實useReducer的原理大概也可以這麼來實現。

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);

  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}
複製程式碼

相信學完這些Hooks的使用後,許多同學都是內心充滿了很多疑惑的同時也想要嘗試看看怎麼使用到實際專案了。

當然現在React官方的建議是:

  • 可以小規模的使用了,但是無需重寫以前的元件實現。React是不會移除class Component這些原有API的。
  • 如果決定使用Hooks的話,可以加上React提供的eslint-plugin-react-hooks,用於檢測對於Hooks的不正當使用。(聽說create-react-app很快將會加上這個配置)
  • 學習與使用React Hooks其實更需要的是換一種心智模型去理解,Hooks更多的像是一個同步處理資料的過程。

Hooks存在的意義以及原因?

傳統元件的開發有以下幾個侷限:

  1. 難以複用含有state(狀態)的元件邏輯。HOC、render props這兩種做法雖然可以解決,但是一是需要重新架構元件,可能會使程式碼更復雜。二是可能會造成wrapper hell。
  2. 複雜元件難以理解消化。因為狀態邏輯、訊息訂閱、請求、以及副作用在不同的生命鉤子混亂穿插,彼此耦合,使得一個元件難以再細化拆分。即使使用了Redux這種狀態管理的庫後,也引進了更高層的抽象,同時需要在不同的檔案之間穿插跳躍,複用元件也不是一件容易的事。
  3. class讓人困惑。(先別急著反對)一個是this讓人困惑,常常需要繫結、第二是class轉譯和壓縮出來的程式碼其實相當冗長。

Hooks的注意事項

  1. 只能在函式的頂層使用,不能巢狀於迴圈體、判斷條件等裡面。原因是因為需要確保Hooks每次在元件渲染中都是按照同樣的順序,這個十分重要,具體原因將會是一個很大的篇幅
  2. 只能在React函式元件裡,或者自定義鉤子(custom Hooks)裡使用。

總結

寫到這裡,文章篇幅已經很長了。一篇文章是說不完Hooks的。學習Hooks的最推薦的其實是看官網文件以及Dan Abramov的博文,以及多多動手實踐。

React Hooks的學習筆記

謝謝大家閱讀~~

相關文章