點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Effect Hook 清理 Side Effect

Sam Xiao's Avatar 2019-10-02

有些需要 Subscribe 的 Side Effect,如 addEventListener(),最後需要做 Unsubscribe 處理,否則會造成 Memory Leak,Effect Hook 也支援這類操作。

Version

macOS Mojave 10.14.6
WebStorm 2019.2.3
Node 12.11.0
Yarn 1.19.0
create-react-app 3.1.2
React 16.10.1

Class Component

import React, { Component } from 'react';

export default class extends Component {
  state = {
    x: null,
    y: null
  };

  componentDidMount = () => {
    window.addEventListener('mousemove', this.handleMouseMove);
  };

  componentWillUnmount = () => {
    window.removeEventListener('mousemove', this.handleMouseMove);
  };

  handleMouseMove = event => {
    this.setState({
      x: event.pageX,
      y: event.pageY
    });
  };

  render() {
    return (
      <div>
        <h2>Mouse Position</h2>
        <p>X position: { this.state.x }</p>
        <p>Y position: { this.state.y }</p>
      </div>
    );
  }
}

24 行

render() {
  return (
    <div>
      <h2>Mouse Position</h2>
      <p>X position: { this.state.x }</p>
      <p>Y position: { this.state.y }</p>
    </div>
  );
}

當滑鼠移動時,會自動顯示其 X 座標與 Y 座標。

第 4 行

state = {
  x: null,
  y: null
};

宣告 xy state,用來紀錄滑鼠移動時的 X 座標與 Y 座標。

第 9 行

componentDidMount = () => {
  window.addEventListener('mousemove', this.onMouseMove);
};

當 component 完成 mount 時,對 browser subscribe mousemove event 的 handler 為 onMouseMove()

17 行

onMouseMove = event => {
  this.setState({
    x: event.pageX,
    y: event.pageY
   });
};

mousemove event 觸發時,會執行 onMouseMove() 將 X 軸座標與 Y 軸座標寫入 xy state。

13 行

componentWillUnmount = () => {
  window.removeEventListener('mousemove', this.handleMouseMove);
};

當 component 完成 unmount 時,對 browser unsubscribe mousemove event。

Class component 的 lifecycle 使得我們將明明是同一件事情的 addEventListener()removeEventListener() 要分開寫,除了很容易忘記外,也較不易維護

Function Component

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

export default () => {
  let [mousePosition, setMousePosition] = useState({ x: null, y: null});

  useEffect(() => {
    window.addEventListener('mousemove', handleMouseMove);

    return () => window.removeEventListener('mousemove', handleMouseMove);
  }, []);

  let handleMouseMove = event => {
    setMousePosition({
      x: event.pageX,
      y: event.pageY
    })
  };

  return (
    <div>
      <h2>Mouse Position</h2>
      <p>X position: { mousePosition.x }</p>
      <p>Y position: { mousePosition.y }</p>
    </div>
  );
};

第 1行

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

useEffect() import 進來。

第 4 行

let [mousePosition, setMousePosition] = useState({ x: null, y: null});

宣告 mousePosition state 為 object,包含 xy property。

第 6 行

useEffect(() => {
  window.addEventListener('mousemove', handleMouseMove);

  return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);

useEffect() 的第一個 argument 除了傳入處理 side effect 的 function 外,若需清理 side effect,只需回傳另一個專門處理 side effect 的 function。

在 class component 我們是在 componentDidMount() 執行 addEventListener()componentWillUnmount() 執行 removeEventListener(),也就是只有在 component 的 mount 與 unmount 才執行。

但若改用 function component,則每次 DOM render 都會執行 useEffect() 的 effect function,雖然結果看起來一樣,但實際上與原本 class component 的意義已經不同。

useEffect() 另外提供了第二個 arguement,每次 DOM render 完,若 [] 內的 state 有變動,則會執行該 effect hook。

因為我們不希望每次 DOM render 就執行 effect hook,因此傳入 [] empty array,如此可避免每次 DOM render 完就執行,等效於 componentDidMount()componentWillUnmount() lifecycle 只執行一次,而不會在 componentDidUpdate() lifecycle 執行。

Function component 的 effect hook 使得我們將同一件事情的 addEventListener()removeEventListener() 寫在同一個 function,除了不容易忘記外,也較易維護

Conclusion

  • 一個 function component 可以有多的 useEffect(),可將相關的 side effect 寫在同一個 useEffect(),而不必如 class component 必須遷就 lifecycle,導致同一件事情要分散在不同 lifecycle
  • 若 side effect 只需在 componentDidMount()componentWillUnmount() lifecycle 執行,須在 useEffect() 的第二個 argument 傳入 [] empty array,表示不依賴任何 state 變動

Reference

Reed Barger, Cleaning up Side Effects in useEffect
React, Using the Effect Hook
React, useEffect()