React-hooks 簡介

什麼是 Hooks?

不經過編寫類組件的狀況下,能夠在組件內部使用狀態(state) 和其餘 React 特性(生命週期,context)的技術react

Hooks 爲何會出現

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

  1. 狀態邏輯難以複用。不少類組件都有一些相似的狀態邏輯,可是爲了重用這些狀態邏輯,社區提出了 render props 或者 hoc 這些方案,可是這兩種模式對組件的侵入性太強。另外,會產生組件嵌套地獄的問題。
  2. 大多數開發者在編寫組件時,無論這個組件有木有內部狀態,會不會執行生命週期函數,都會將組件編寫成類組件,後續迭代可能增長了內部狀態,又增長了反作用處理,又在組件中調用了一些生命週期函數,文件代碼行很多天益增多,最後致使組件中充斥着沒法管理的混亂的狀態邏輯代碼和各類反作用,各類狀態邏輯散落在實例方法和生命週期方法中,維護性變差,拆分更是難上加難。
  3. 在類組件中,須要開發者額外去關注 this 問題,事件監聽器的添加和移除等等。

State Hook

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

一個簡單的使用 useState 栗子github

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>
  );
};

複製代碼

幾點說明:redux

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

  2. useState 的執行順序在每一次更新渲染時必須保持一致,不然多個 useState 調用將不會獲得各自獨立的狀態,也會形成狀態對應混亂。好比在條件判斷中使用 hook,在循環,嵌套函數中使用 hook,都會形成 hook 執行順序不一致的問題。最後致使狀態的混亂。另外,全部的狀態聲明都應該放在函數頂部,首先聲明。數組

  3. useStatesetState 的區別網絡

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

一個不正確的栗子async

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 的討論

相關文章
相關標籤/搜索