react-hooks中的一些懵逼點

前言:一直對這個新特性非常感興趣,終於今天有時間,花了大半天時間,把 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 第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

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章