跟著 React 官方文件學 Hooks

zenghongtu發表於2019-05-14

這篇文章兩個月之前寫的,看了一下官網文件沒啥變化,就發出來了。如果有什麼錯誤,歡迎指出~

前言:一直對這個新特性非常感興趣,終於今天有時間,花了大半天時間,把 Hooks的官方教程過了一遍,收穫頗多,驚歎這個新特性真 TM 好用,以後開發用這個怕是要起飛了?。

狀態鉤子(State Hook)

const [state, setState] = useState(initialState);
複製程式碼
  1. 多個useState時,React依賴於每次渲染時鉤子的呼叫順序都是一樣的(存在與每個元件關聯的“儲存單元”的內部列表存放JavaScript物件),從而實現鉤子與狀態的一一對應關係。
  2. setState()接收新的state或者一個返回state的函式(setCount(prevCount => prevCount - 1)})。
  3. 不同於類元件中的setStateuseState返回的setState 不會自動合併更新物件到舊的state中(可以使用useReducer)。
  4. useState可以接收一個函式返回initialState,它只會在初次渲染時被呼叫。
  5. setState中的state和當前的state相等(通過Object.is判斷),將會退出更新。
  6. 建議將一個狀態根據哪些需要值一起變化拆分為多個狀態變數。
const [rows, setRows] = useState(createRows(props.count));  // `createRows()`每次將會渲染將會被呼叫
複製程式碼

優化一下:

const [rows, setRows] = useState(() => createRows(props.count));  // `createRows()`只會被呼叫一次
複製程式碼

其中的() => createRows(props.count)會賦值給rows,這樣就保證了只有在rows呼叫時,才會建立新的值。

作用鉤子(Effect Hook)

useEffect(didUpdate);
複製程式碼
  1. 相當於生命週期函式componentDidMount, componentDidUpdate, componentWillUnmount的組合。
  2. 可以返回一個函式(cleanup)用於清理。
  3. 每次重新渲染都將會發生cleanup phase
useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
複製程式碼
  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  // ====== 原因在這裡 ======
  componentDidUpdate(prevProps) {
    // Unsubscribe from the previous friend.id
    ChatAPI.unsubscribeFromFriendStatus(
      prevProps.friend.id,
      this.handleStatusChange
    );
    // Subscribe to the next friend.id
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
複製程式碼
// Mount with { friend: { id: 100 } } props
ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // Run first effect

// Update with { friend: { id: 200 } } props
ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // Run next effect

// Update with { friend: { id: 300 } } props
ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // Clean up previous effect
ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // Run next effect

// Unmount
ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // Clean up last effect
複製程式碼
  1. useEffect(() => {document.title = You clicked ${count} times;}, [count]); ,指定第二個引數(這裡為[count])變化時才發生cleanup phase,然後執行effect
  2. 上面情況,如果useEffect第二個引數為為[]則表示只執行一次(componentDidMount中執行effectcomponentWillUnmount中進行cleanup),永遠不重新執行。
  3. componentDidMount/componentDidUpdate有區別的地方在於,useEffect中的函式會在layoutpaint結束後才被觸發。(可以使用useLayoutEffect在下一次渲染之前(即 DOM 突變之後)同步觸發)
  4. useEffect雖然被推遲到瀏覽器繪製完成之後,但是肯定在有任何新的呈現之前啟動。因為React總是在開始更新之前重新整理之前渲染的效果。

其他鉤子

useContext

const context = useContext(Context);
複製程式碼

接受一個上下文物件(由React.createContext建立),返回當前上下文值(由最近的上下文提供)。

附加鉤子(Additional Hooks)

基本鉤子的變體或用於特定邊緣情況的鉤子。

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);
複製程式碼
  1. 第三個引數init為函式,將會這樣呼叫:init(initialArg),返回初始值。
  2. 如果返回state和現在的state一樣,將會在不影響子孫或者觸發效果的情況下退出渲染。

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);
複製程式碼

傳入一個內聯回撥和一個輸入陣列,返回一個帶有記憶的函式,只有輸入陣列中其中一個值變化才會更改。useCallback(fn, inputs) 等價於 useMemo(() => fn, inputs)

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
複製程式碼

傳入一個建立函式和一個輸入陣列,返回一個帶有記憶的,只有輸入陣列中其中一個值變化才會重新計算。

useRef

const refContainer = useRef(initialValue);
// ...
<input ref={refContainer} />
...
複製程式碼

返回一個可變的ref物件,可以自動將ref物件中的current屬性作為初始值傳遞的引數,保持到元件的整個生命週期。

與在類中使用例項欄位的方式類似,它可以保留任何可變值

如儲存前一個狀態:

function Counter() {
  const [count, setCount] = useState(0);

  const prevCountRef = useRef();
  useEffect(() => {
    prevCountRef.current = count;
  });
  const prevCount = prevCountRef.current;

  return <h1>Now: {count}, before: {prevCount}</h1>;
}
複製程式碼

useImperativeHandle

useImperativeHandle(ref, createHandle, [inputs])
複製程式碼

自定在使用 ref 時,公開給父元件的例項值,必須和forwardRef一起使用。

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

複製程式碼
<FancyInput ref={fancyInputRef} />

// 呼叫
fancyInputRef.current.focus()
複製程式碼

useLayoutEffect

使用方法和useLayoutEffect一致,不過它是在 DOM 讀取佈局時同步觸發(相當於componentDidMountcomponentDidUpdate階段)。(建議儘可能使用useEffect避免阻塞視覺化更新)

useDebugValue

useDebugValue(value)
複製程式碼

用於在React DevTools中顯示自定義鉤子的標籤,對於自定義鉤子中用於共享的部分有更大價值。

自定義顯示格式:

useDebugValue(date, date => date.toDateString());
複製程式碼

鉤子(Hooks)規則

1. 只能在頂層呼叫,不能再迴圈、條件語句和巢狀函式中使用。 (原因:[State Hook](#State Hook) 第1條)

正確做法:

useEffect(function persistForm() {
      // ? We're not breaking the first rule anymore
      if (name !== '') {
        localStorage.setItem('formData', name);
      }
    });
複製程式碼

2. 只能在React函式元件中被呼叫。(可以通過自定義鉤子函式解決)

可以使用eslint-plugin-react-hooks來強制自動執行這些規則。

自定義鉤子(Hook)

  1. use開頭,一種公約。
  2. 自定鉤子是一種複用狀態邏輯的機制(例如設定訂閱和記住當前值),每次使用,內部所有狀態和作用都是獨立的。
  3. 自定義鉤子每個狀態獨立的能力源於useStateuseEffect是完全獨立的。

測試鉤子(Hook)

function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
複製程式碼

使用ReactTestUtils.act()

import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import Counter from './Counter';

let container;

beforeEach(() => {
  container = document.createElement('div');
  document.body.appendChild(container);
});

afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

it('can render and update a counter', () => {
  // Test first render and effect
  act(() => {
    ReactDOM.render(<Counter />, container);
  });
  const button = container.querySelector('button');
  const label = container.querySelector('p');
  expect(label.textContent).toBe('You clicked 0 times');
  expect(document.title).toBe('You clicked 0 times');

  // Test second render and effect
  act(() => {
    button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
  });
  expect(label.textContent).toBe('You clicked 1 times');
  expect(document.title).toBe('You clicked 1 times');
});
複製程式碼

建議使用react-testing-library

參考

相關文章