3
3

ReactのuseEffectのダークサイド

Last updated at Posted at 2024-08-01

ReactのuseEffectフックは、コンポーネントの副作用を管理するための強力なツールです。
だけど、このフックには見落としがちな落とし穴や注意が必要な側面が存在します。

この一見無害なReactコンポーネントを見てみましょう。
以下のコードには重大なエラーがあります。

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

function DarkSideOfUseEffect() {
  const [count, setCount] = useState(0);
  const [pressed, setPressed] = useState(false);

  useEffect(() => {
    console.log('Count is', count);
    setPressed(!pressed);
  });

  // ...他のコンポーネントロジック...

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default DarkSideOfUseEffect;

お分かりいただけたでしょうか?

多くのReact開発者は、少なくとも一度はこのミスを犯したことがあるんじゃかな。そして、このミスは望まない無限ループやメモリリークを引き起こし、アプリケーションを破壊してしまいます。

隠れたエラーは、useEffectブロックにある。これを修正するには、依存配列を含める必要があります。
ということで、以下のように書き換えてみます。

コード内で状態が変わった場合にのみuseEffectを実行する🔽

React
useEffect(() => {
    console.log('Count is', count);
    setPressed(!pressed);
    // 以下の空の配列は、ReactにuseEffect()フック内のコードを
    // 一度だけ実行するように指示します。
  }, []);

APIへのDDOS攻撃

APIに無限のGETリクエストを送り続けるのは、基本的にDDOS攻撃と呼ばれますね。

これはAPIをダウンさせるだけでなく、例えばAWS上にReactアプリをホストしていてレートリミティングがない場合、大きなコストを被ることになってしまいます。

コードのどこかに依存配列を含め忘れたせいで、Amazonから$1000の請求書を受け取るのは、どうやっても避けたい……

❌次のコードは間違っていて、APIを無限にピンし続けるから注意が必要しましょう。

React
const [data, setData] = useState()
useEffect(() => {
    async function getData() {
      const fetchedData = await fetch("https://example.com/api")
      setData(fetchedData)
      console.log(data)
    }
    getData()
});

✅代わりに、次のようにします🔽

React
const [data, setData] = useState()
useEffect(() => {
    // このコンポーネントがマウントされたときにデータを安全にフェッチします
    async function getData() {
      const fetchedData = await fetch("https://example.com/api")
      setData(fetchedData)
      console.log(data)
    }
    getData()
}, []);

無限発生UI

useEffectフックの一般的な使用例は、状態変数が変化したときにUIを変更することです。

しかし、以下のコードでは、useEffectが各レンダリング後に(ボタンが押されたときに状態が更新される場合も含めて)実行されるため、

要素の無限のちらつきが発生します🔽

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

function FlickeringComponent() {
  const [isVisible, setIsVisible] = useState(true);

  // 間違った使用例 ❌: useEffectブロック内で状態を変更する
  useEffect(() => {
    setIsVisible(!isVisible);
  }, [isVisible]);

  return (
    <div>
      {isVisible ? <p>Visible Content</p> : null}
      <button onClick={() => setIsVisible(!isVisible)}>
        Toggle Visibility
      </button>
    </div>
  );
}

export default FlickeringComponent;

これを修正するには、依存配列を空の配列[]に置き換える必要があります。

あるいは、useEffectフックを削除し、isVisibleをfalseで初期化するだけで、コードをよりクリーンにすることができます🔽

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

function StableComponent() {
  // 初期状態をfalseに設定し、コンテンツを非表示にする
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      {isVisible ? <p>Stable Content</p> : null}
      <button onClick={() => setIsVisible(!isVisible)}>
        Toggle Visibility
      </button>
    </div>
  );
}

export default StableComponent;

こちらのコードなら、無限ループを防ぎ、期待通りに動作します。

チャットアプリケーションにおける注意点

メッセージのリストが状態に保持され、Webソケットを介して同期されているとします。

以下は、メッセージの状態を取得および更新するためにuseEffectフックを誤って使用する例です🔽

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

function ChatComponent({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // roomIdに基づいてメッセージを取得
    fetchMessages(roomId)
      .then((newMessages) => {
        // ミス ❌: 既存のメッセージを上書きする
        setMessages(newMessages);
      })
      .catch((error) => {
        console.error('Error fetching messages:', error);
      });
  }, [roomId]);

  return (
    <div>
      <h2>Chat Room {roomId}</h2>
      <ul>
        {messages.map((message) => (
          <li key={message.id}>{message.content}</li>
        ))}
      </ul>
    </div>
  );
}

export default ChatComponent;

一見正しく見えますが、useEffect内に論理エラーがあります。これにより、メッセージ配列が完全に上書きされてしまいます。

つまり、リアルタイムで他のユーザーによって書き込まれる他のメッセージが最新のレンダリングによって上書きされてしまうということです。

これを修正するには、新しいメッセージを既存のメッセージと結合し、メッセージ状態全体を上書きしないようにする必要があります🔽

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

function ChatComponent({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    // roomIdに基づいてメッセージを取得
    fetchMessages(roomId)
      .then((newMessages) => {
        // 正しい方法 ✅: 新しいメッセージを既存のメッセージと結合する
        setMessages((prevMessages) => [...prevMessages, ...newMessages]);
      })
      .catch((error) => {
        console.error('Error fetching messages:', error);
      });
  }, [roomId]);

  return (
    <div>
      <h2>Chat Room {roomId}</h2>
      <ul>
        {messages.map((message) => (
          <li key={message.id}>{message.content}</li>
        ))}
      </ul>
    </div>
  );
}

export default ChatComponent;

お疲れさまでした

わからないところ、間違っているところ、もっといい方法がある場合は、コメントでもDMでも教えてください。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3