React-hooks 簡介

勵步前端團隊發表於2019-05-04

什麼是 Hooks?

不通過編寫類元件的情況下,可以在元件內部使用狀態(state) 和其他 React 特性(生命週期,context)的技術

Hooks 為什麼會出現

在之前的 React 版本中,元件分為兩種:函式式元件(或無狀態元件(StatelessFunctionComponent))和類元件,而函式式元件是一個比較的純潔的 props => UI 的輸入、輸出關係,但是類元件由於有元件自己的內部狀態,所以其輸出就由 propsstate 決定,類元件的輸入、輸出關係就不再那麼純潔。同時也會帶來下列問題:

  1. 狀態邏輯難以複用。很多類元件都有一些類似的狀態邏輯,但是為了重用這些狀態邏輯,社群提出了 render props 或者 hoc 這些方案,但是這兩種模式對元件的侵入性太強。另外,會產生元件巢狀地獄的問題。
  2. 大多數開發者在編寫元件時,不管這個元件有木有內部狀態,會不會執行生命週期函式,都會將元件編寫成類元件,後續迭代可能增加了內部狀態,又增加了副作用處理,又在元件中呼叫了一些生命週期函式,檔案程式碼行數日益增多,最後導致元件中充斥著無法管理的混亂的狀態邏輯程式碼和各種副作用,各種狀態邏輯散落在例項方法和生命週期方法中,維護性變差,拆分更是難上加難。
  3. 在類元件中,需要開發者額外去關注 this 問題,事件監聽器的新增和移除等等。

State Hook

state hook 提供了一種可以在 function component 中新增狀態的方式。通過 state hook,可以抽取狀態邏輯,使元件變得可測試,可重用。開發者可以在不改變元件層次結構的情況下,去重用狀態邏輯。更好的實現關注點分離。

一個簡單的使用 useState 栗子

import React, { useState } from "react";

const StateHook = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>you clicked {count} times</p>
      <button type="button" onClick={() => setCount(count + 1)}>
        click me
      </button>
    </div>
  );
};

複製程式碼

幾點說明:

  1. useState 推薦一種更加細粒度的控制狀態的方式,即一個狀態對應一個狀態設定函式,其接受的引數將作為這個狀態的初始值。其返回一個長度為2的元組,第一項為當前狀態,第二項為更新函式。

  2. useState 的執行順序在每一次更新渲染時必須保持一致,否則多個 useState 呼叫將不會得到各自獨立的狀態,也會造成狀態對應混亂。比如在條件判斷中使用 hook,在迴圈,巢狀函式中使用 hook,都會造成 hook 執行順序不一致的問題。最後導致狀態的混亂。另外,所有的狀態宣告都應該放在函式頂部,首先宣告。

  3. useStatesetState 的區別

useStatesetState 進行覆蓋式更新,而 setState 則將狀態進行合併式更新。

一個不正確的栗子

import React, { useState, ChangeEvent } from "react";

const UserForm = () => {
  const [state, setUser] = useState({ name: "", email: "" });

  const { name, email } = state;

  const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { target: { value: name } } = event;
    // 這裡不可以單獨的設定某一個欄位 新的狀態必須與初始的狀態型別保持一致
    // 如果只設定了其中一個欄位,編譯器會報錯,同時其餘的欄位也會丟失
    setUser({ name, email });
  };

  const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { target: { value: email } } = event;
    // 這裡不可以單獨的設定某一個欄位 新的狀態必須與初始的狀態型別保持一致
    setUser({ name, email });
  };

  return (
    <>
    	<input value={name} onChange={handleNameChange} />
      <input value={email} onChange={handleEmailChange} />
    </>
  );
}
複製程式碼

正確的做法

import React, { useState, ChangeEvent } from "react";

const UserForm = () => {
  // 一個狀態對應一個狀態更新函式
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { target: { value: name } } = event;
    // hear could do some validation
    setName(name);
  };

  const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { target: { value: email } } = event;
    // hear could do some validation
    setEmail(email);
  };

  return (
    <>
    	<input value={name} onChange={handleNameChange} />
      <input value={email} onChange={handleEmailChange} />
    </>
  );
}
複製程式碼

Effect Hook

資料獲取,設定訂閱,手動的更改 DOM,都可以稱為副作用,可以將副作用分為兩種,一種是需要清理的,另外一種是不需要清理的。比如網路請求,DOM 更改,日誌這些副作用都不要清理。而比如定時器,事件監聽。

一個簡單使用 effect hook 去修改文件標題的栗子。

import React, { useState, useEffect } from "react";

const effectHook = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `you clicked ${count} times`;
  }, [count]);

  return (
    <div>
      <p>you clicked {count} times</p>
      <button type="button" onClick={() => setCount(count + 1)}>
        click me
      </button>
    </div>
  );
};
複製程式碼

在呼叫 useEffect 後,相當於告訴 React 在每一次元件更新完成渲染後,都呼叫傳入 useEffect 中的函式,包括初次渲染以及後續的每一次更新渲染。

幾點說明:

  1. useEffect(effectCallback: () => void, deps: any[]) 接收兩個引數,第二個引數依賴項是可選的,表示這個 effect 要依賴哪些值。
  2. 有時候我們並不想每次渲染 effect 都執行,只有某些值發生變化才去執行 effect,這個時候我們可以指定這個 effect 的依賴列表,可以是一個也可以幾個,當其中列表中的某一個值發生變化,effect 才會執行。
  3. 第一個引數的返回值,會在元件解除安裝時執行,相當於 componentWillUnmount,可以清理定時器,移除事件監聽,取消一些訂閱。
  4. 當第二個引數為一個空陣列時,相當於 componentDidMount 和 componentWillUnmount,表明這個 effect 沒有任何依賴,只在首次渲染時執行。

Custom Hook

也可以使用 useEffectuseState 實現自定義 hook。

一個給 DOM 元素新增事件監聽器的栗子。

import { useRef, useEffect } from "react";

type EventType = keyof HTMLElementEventMap;
type Handler = (event: Event) => void;

const useEventListener = (
  eventName: EventType,
  handler: Handler,
  element: EventTarget = document
) => {
  // 這裡使用 `useRef` 來儲存傳入的監聽器,
  // 在監聽器變更後,去更新 `useRef` 返回的物件的 `current` 屬性
  const saveHandler = useRef<Handler>();

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

  useEffect(() => {
    const supported = element && element.addEventListener;
    if (!supported) {
      return;
    }

    const listener: Handler = (event: Event) => (saveHandler.current as Handler)(event);

    element.addEventListener(eventName, listener);

    return () => {
      element.removeEventListener(eventName, listener);
    };
  }, [eventName, element]);
}
複製程式碼

一個使用 useReducer 來實現加、減計數器的栗子。這裡雖然使用 useReducer 建立了類似 redux 的 功能,但是如果有多個元件都引用了這個 hook,那麼這個 hook 提供的狀態是相互獨立、互不影響的,即 useReducer 只提供了狀態管理,但是並沒有提供資料持久化的功能。redux 卻提供了一種全域性維護同一個資料來源的機制。所以可以利用 useReducerContext 來實現資料持久化的功能。

import React, { useReducer } from "react";

const INCREMENT = "increment";
const DECREMENT = "decrement";

const initHandle = (initCount) => {
  return { count: initCount };
};

const reducer = (state, action) => {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    case "reset":
      return { count: action.payload };
    default:
      return state;
  }
};

const Counter = ({ initialCount }) => {
  const [state, dispatch] = useReducer(reducer, initialCount, initHandle);
  const { count } = state;

  return (
    <div>
      Counter: {count}
      <button type="button" onClick={() => dispatch({ type: "reset", payload: initialCount })}>
        Reset
      </button>
      <button type="button" onClick={() => dispatch({ type: INCREMENT })}>
        +
      </button>
      <button type="button" onClick={() => dispatch({ type: DECREMENT })}>
        -
      </button>j
    </div>
  );
};
複製程式碼

一個對封裝資料請求栗子。

import { useState, useEffect } from "react";
import axios, { AxiosRequestConfig } from "axios";

interface RequestError {
  error: null | boolean;
  message: string;
}

const requestError: RequestError = {
  error: null,
  message: "",
};

/**
 * @param url request url
 * @param initValue if initValue changed, the request will send again
 * @param options request config data
 *
 * @returns a object contains response's data, request loading and request error
 */
const useFetchData = (url: string, initValue: any, options: AxiosRequestConfig = {}) => {
  const [data, saveData] = useState();
  const [loading, updateLoading] = useState(false);
  const [error, updateError] = useState(requestError);

  let ignore = false;

  const fetchData = async () => {
    updateLoading(true);

    const response = await axios(url, options);

    if (!ignore) saveData(response.data);

    updateLoading(false);
  };

  useEffect(() => {
    try {
      fetchData();
    } catch (error) {
      updateError({ error: true, message: error.message });
    }

    return () => {
      ignore = true;
    };
  }, [initValue]);

  return { data, loading, error };
};

export { useFetchData };

複製程式碼

Rules of Hook

隨來 hooks 帶來了新的元件編寫正規化,但是下面兩條規則還是要開發者注意的。

  1. 在頂部使用 hook,不要使用 hook 在條件判斷,迴圈,巢狀函式。
  2. 只在 function component 中使用 hook,或者自定義 hook 中使用 hook, 不要在常規的 JavaScript 函式中使用 hook

新的問題

hooks 的帶來,雖然解決之前存在的一些問題,但是也帶來了新的問題。

  1. 異常捕獲。之前的版本中,我們可以用 componentDidCatch 來捕獲元件作用域內的異常,做一些提示。但是在 hooks 中 ,我們只能使用 try {} catch(){} ` 去捕獲,使用姿勢也比較彆扭。
  2. 一個元件若有狀態,則狀態一旦改變,所有的子元件需要重新渲染。所以一個有狀態的元件,應該是沒有子元件的。即 有狀態的元件不做渲染,有渲染的元件沒有狀態
  3. 狀態變更的函式不支援回撥。this.setState() 中支援第二個引數,允許我們在狀態變更後,傳入回撥函式做一些其他事情。但是 useState 不支援。詳見

連結**

making-sense-of-react-hooks

rehooks

awesome-react-hooks

如何使用useEffect來獲取資料

hooks 是如何工作的

更多關於 hooks 的討論