點燈坊

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

根據 State 改變才執行 Effect Hook

Sam Xiao's Avatar 2019-10-02

useEffect() 的第二個 Argument 可傳入 Array,指定當特定 State 改變時,才執行 Effect Function。

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

Empty State Dependency

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

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

  useEffect(() => {
    document.title = `You have clicked ${ count } times`;
    window.addEventListener('mousemove', handleMouseMove);

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

  let addCount = () => setCount(count + 1);

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

  return (
    <div>
      <button onClick={ addCount }>+</button>
      <div>{ count }</div>
      <h2>Mouse Position</h2>
      <p>X position: { mousePosition.x }</p>
      <p>Y position: { mousePosition.y }</p>
    </div>
  );
};

第 7 行

useEffect(() => {
  document.title = `You have clicked ${ count } times`;
  window.addEventListener('mousemove', handleMouseMove);

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

Effect function 內含有兩個 side effect,一個是改變 document.title,另一個使用 window.addEventListener() 註冊 mousemove event。

為了讓註冊 mousemove event 只在 componentDidmount()componentWillUnmount() lifecycle 執行,而不會在 componentDidUpdate() lifecycle 執行,所以特別在 useEffect() 的第二個 argument 傳入 [] empty array,表示任何 state 變動都不會執行 effection function。

但實際執行會發現這導致了 document.title 也不再因為 count state 改變而改變了。

State Dependency

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

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

  useEffect(() => {
    document.title = `You have clicked ${ count } times`;
    window.addEventListener('mousemove', handleMouseMove);

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

  let addCount = () => setCount(count + 1);

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

  return (
    <div>
      <button onClick={ addCount }>+</button>
      <div>{ count }</div>
      <h2>Mouse Position</h2>
      <p>X position: { mousePosition.x }</p>
      <p>Y position: { mousePosition.y }</p>
    </div>
  );
};

第 7 行

useEffect(() => {
  document.title = `You have clicked ${ count } times`;
  window.addEventListener('mousemove', handleMouseMove);

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

解決方式是在 useEffect() 的第二個 argument 以 [count] 傳入,表示 effect function 除了在 componentDidMount()componentWillUnMount() lifecyle 執行外,在 componentDidUpate() 時當count state 改變時,就會執行 effect function。

Separate useState()

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

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

  useEffect(() => {
    document.title = `You have clicked ${ count } times`;
  });

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

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

  let addCount = () => setCount(count + 1);

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

  return (
    <div>
      <button onClick={ addCount }>+</button>
      <div>{ count }</div>
      <h2>Mouse Position</h2>
      <p>X position: { mousePosition.x }</p>
      <p>Y position: { mousePosition.y }</p>
    </div>
  );
};

第 7 行

useEffect(() => {
  document.title = `You have clicked ${ count } times`;
});

將改變 document.title 的 side effect 獨立出來,如此每次 count 改變 document.title 都會改變。

11 行

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

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

將註冊 mousemove event 的 side effect 獨立出來,如此只有 componentDidMount()componentWillUnmount() lifecycle 才會執行。

Conclusion

  • 雖然可將多個 side effect 寫在一起,透過 useEffect() 的第二個 argument 指定要相依的 state 改變,但實務上還是建議分多個 useEffect() 寫,維持一個 useEffect() 一個 side effect,這更符合原本 effect hook 設計本意,也更為清楚

Reference

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