Skip to content

Latest commit

 

History

History
1862 lines (1466 loc) · 76.6 KB

File metadata and controls

1862 lines (1466 loc) · 76.6 KB
title useEffect

useEffect は、コンポーネントを外部システムと同期させるための React フックです。

useEffect(setup, dependencies?)

リファレンス {/reference/}

useEffect(setup, dependencies?) {/useeffect/}

コンポーネントのトップレベルで useEffect を呼び出して、エフェクト (Effect) を宣言します。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  // ...
}

さらに例を見る

引数 {/parameters/}

  • setup: エフェクトのロジックが記述された関数です。このセットアップ関数は、オプションでクリーンアップ関数を返すことができます。コンポーネントが初めて DOM に追加されると、React はセットアップ関数を実行します。依存配列 (dependencies) が変更された再レンダー時には、React はまず古い値を使ってクリーンアップ関数(あれば)を実行し、次に新しい値を使ってセットアップ関数を実行します。コンポーネントが DOM から削除された後、React はクリーンアップ関数を最後にもう一度実行します。

  • 省略可能 dependencies: setup コード内で参照されるすべてのリアクティブな値のリストです。リアクティブな値には、props、state、コンポーネント本体に直接宣言されたすべての変数および関数が含まれます。リンタが React 用に設定されている場合、すべてのリアクティブな値が依存値として正しく指定されているか確認できます。依存値のリストは要素数が一定である必要があり、[dep1, dep2, dep3] のようにインラインで記述する必要があります。React は、Object.is を使った比較で、それぞれの依存値を以前の値と比較します。この引数を省略すると、エフェクトはコンポーネントの毎回のレンダー後に再実行されます。依存値の配列を渡す場合と空の配列を渡す場合、および何も渡さない場合の違いを確認してください。

返り値 {/returns/}

useEffectundefined を返します。

注意点 {/caveats/}

  • useEffect はフックであるため、コンポーネントのトップレベルやカスタムフック内でのみ呼び出すことができます。ループや条件文の中で呼び出すことはできません。これが必要な場合は、新しいコンポーネントを抽出し、その中に state を移動させてください。

  • 外部システムと同期する必要がない場合エフェクトはおそらく不要です

  • Strict Mode が有効な場合、React は本物のセットアップの前に、開発時専用のセットアップ+クリーンアップサイクルを 1 回追加で実行します。これは、クリーンアップロジックがセットアップロジックと鏡のように対応しており、セットアップで行われたことを停止または元に戻していることを保証するためのストレステストです。問題が発生した場合は、クリーンアップ関数を実装します

  • 依存配列の一部にコンポーネント内で定義されたオブジェクトや関数がある場合、エフェクトが必要以上に再実行される可能性があります。これを修正するには、オブジェクト型および関数型の不要な依存値を削除します。また、エフェクトの外部に state の更新非リアクティブなロジックを抽出することもできます。

  • エフェクトがユーザ操作(クリックなど)によって引き起こされたものでない場合、React は通常、ブラウザが新しい画面を描画した後にエフェクトを実行します。あなたのエフェクトが(ツールチップの配置など)何か視覚的な作業を行っており遅延が目立つ場合(ちらつくなど)、useEffectuseLayoutEffect に置き換えてください

  • エフェクトがユーザ操作(クリックなど)によって引き起こされた場合、React はブラウザが更新後の画面を描画する前にエフェクトを実行することがあります。これによりエフェクトの結果がイベントシステムに見えることが保証されます。これは通常は期待通りに動作します。しかし、alert() のように描画後まで作業を遅らせる必要がある場合は、setTimeout を使用できます。詳細については、reactwg/react-18/128 を参照してください。

  • エフェクトがユーザ操作(クリックなど)によって引き起こされた場合、React はエフェクト内で起きた state 更新を処理する前に、ブラウザに画面を再描画させることがあります。これは通常は期待通りに動作します。しかし、ブラウザによる画面の再描画をブロックしなければならない場合は、useEffectuseLayoutEffect に置き換える必要があります。

  • エフェクトはクライアント上でのみ実行されます。サーバレンダリング中には実行されません。


使用法 {/usage/}

外部システムへの接続 {/connecting-to-an-external-system/}

コンポーネントによっては自身がページに表示されている間、ネットワーク、何らかのブラウザ API、またはサードパーティライブラリとの接続を維持する必要があるものがあります。これらのシステムは React によって制御されていないため、外部 (external) のものです。

コンポーネントを外部システムに接続するには、コンポーネントのトップレベルで useEffect を呼び出します。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
}

useEffect には 2 つの引数を渡す必要があります。

  1. システムに接続するセットアップコードを含むセットアップ関数
    • そのシステムから切断するクリーンアップコードを含むクリーンアップ関数を返す必要があります。
  2. これらの関数内で使用されるコンポーネントからのすべての値を含んだ依存値のリスト。

React は必要に応じてセットアップ関数とクリーンアップ関数を呼び出し、これは複数回行われることがあります。

  1. コンポーネントがページに追加(マウント)されると、セットアップコードが実行されます。
  2. 依存値が変更された上でコンポーネントが再レンダーされる度に:
    • まず、古い props と state でクリーンアップコードが実行されます。
    • 次に、新しい props と state でセットアップコードが実行されます。
  3. コンポーネントがページから削除(アンマウント)されると、最後にクリーンアップコードが実行されます。

上記の例でこのシーケンスを説明しましょう。

上記の ChatRoom コンポーネントがページに追加されると、serverUrlroomId の初期値を使ってチャットルームに接続します。serverUrl または roomId が再レンダーの結果として変更される場合(例えば、ユーザがドロップダウンで別のチャットルームを選択した場合)、あなたのエフェクトは以前のルームから切断し、次のルームに接続しますChatRoom コンポーネントがページから削除されると、あなたのエフェクトは最後の切断を行います。

バグを見つけ出すために、開発中には React はセットアップとクリーンアップを、セットアップの前に 1 回余分に実行します。これは、エフェクトのロジックが正しく実装されていることを確認するストレステストです。これが目に見える問題を引き起こす場合、クリーンアップ関数に一部のロジックが欠けています。クリーンアップ関数は、セットアップ関数が行っていたことを停止ないし元に戻す必要があります。基本ルールとして、ユーザはセットアップが一度しか呼ばれていない(本番環境の場合)か、セットアップクリーンアップセットアップのシーケンス(開発環境の場合)で呼ばれているかを区別できないようにする必要があります。一般的な解決法を参照してください

各エフェクトを独立したプロセスとして記述するようにし、一回のセットアップ/クリーンアップのサイクルだけを考えるようにしてください。コンポーネントが現在マウント、更新、アンマウントのどれを行っているかを考慮すべきではありません。セットアップロジックが正しくクリーンアップロジックと「対応」されることで、エフェクトはセットアップとクリーンアップを必要に応じて何度実行しても問題が起きない、堅牢なものとなります。

エフェクトは、コンポーネントを外部システム(チャットサービスのようなもの)と同期させるために使います。ここでいう外部システムとは、React が制御していないコードの一部で、たとえば以下のようなものです。

外部システムに接続していない場合は、恐らくエフェクトは不要です

チャットサーバへの接続 {/connecting-to-a-chat-server/}

この例では、ChatRoom コンポーネントがエフェクトを使って chat.js で定義された外部システムに接続しています。"Open chat" を押すと ChatRoom コンポーネントが表示されます。このサンドボックスは開発モードで実行されているため、こちらで説明されているように、接続と切断のサイクルが 1 回追加で発生します。roomIdserverUrl をドロップダウンと入力欄で変更して、エフェクトがチャットに再接続する様子を確認してみてください。"Close chat" を押すと、エフェクトが最後の 1 回の切断作業を行います。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }

グローバルなブラウザイベントへのリッスン {/listening-to-a-global-browser-event/}

この例では、外部システムはブラウザの DOM 自体です。イベントリスナは通常 JSX で指定しますが、この方法ではグローバルな window オブジェクトへはリッスンすることはできません。エフェクトを使うことで、window オブジェクトに接続し、そのイベントをリッスンできます。pointermove イベントにリッスンすることで、カーソル(または指)の位置を追跡し、赤い点をそれに合わせて移動させることができます。

import { useState, useEffect } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    function handleMove(e) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
    window.addEventListener('pointermove', handleMove);
    return () => {
      window.removeEventListener('pointermove', handleMove);
    };
  }, []);

  return (
    <div style={{
      position: 'absolute',
      backgroundColor: 'pink',
      borderRadius: '50%',
      opacity: 0.6,
      transform: `translate(${position.x}px, ${position.y}px)`,
      pointerEvents: 'none',
      left: -20,
      top: -20,
      width: 40,
      height: 40,
    }} />
  );
}
body {
  min-height: 300px;
}

アニメーションのトリガ {/triggering-an-animation/}

この例では、外部システムは animation.js に書かれているアニメーションライブラリです。これは、DOM ノードを引数に取る FadeInAnimation という JavaScript クラスを提供し、アニメーションを制御するための start() および stop() メソッドを公開しています。このコンポーネントは、背後にある DOM ノードにアクセスするために ref を使います。エフェクトは ref から DOM ノードを読み取り、コンポーネントが表示されたときにそのノードのアニメーションを自動的に開始します。

import { useState, useEffect, useRef } from 'react';
import { FadeInAnimation } from './animation.js';

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(1000);
    return () => {
      animation.stop();
    };
  }, []);

  return (
    <h1
      ref={ref}
      style={{
        opacity: 0,
        color: 'white',
        padding: 50,
        textAlign: 'center',
        fontSize: 50,
        backgroundImage: 'radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%)'
      }}
    >
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}
export class FadeInAnimation {
  constructor(node) {
    this.node = node;
  }
  start(duration) {
    this.duration = duration;
    if (this.duration === 0) {
      // Jump to end immediately
      this.onProgress(1);
    } else {
      this.onProgress(0);
      // Start animating
      this.startTime = performance.now();
      this.frameId = requestAnimationFrame(() => this.onFrame());
    }
  }
  onFrame() {
    const timePassed = performance.now() - this.startTime;
    const progress = Math.min(timePassed / this.duration, 1);
    this.onProgress(progress);
    if (progress < 1) {
      // We still have more frames to paint
      this.frameId = requestAnimationFrame(() => this.onFrame());
    }
  }
  onProgress(progress) {
    this.node.style.opacity = progress;
  }
  stop() {
    cancelAnimationFrame(this.frameId);
    this.startTime = null;
    this.frameId = null;
    this.duration = 0;
  }
}
label, button { display: block; margin-bottom: 20px; }
html, body { min-height: 300px; }

モーダルダイアログの制御 {/controlling-a-modal-dialog/}

この例では、外部システムはブラウザの DOM です。ModalDialog コンポーネントは <dialog> 要素をレンダーします。isOpen プロパティを showModal() および close() メソッド呼び出しに同期させるためにエフェクトを使用しています。

import { useState } from 'react';
import ModalDialog from './ModalDialog.js';

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(true)}>
        Open dialog
      </button>
      <ModalDialog isOpen={show}>
        Hello there!
        <br />
        <button onClick={() => {
          setShow(false);
        }}>Close</button>
      </ModalDialog>
    </>
  );
}
import { useEffect, useRef } from 'react';

export default function ModalDialog({ isOpen, children }) {
  const ref = useRef();

  useEffect(() => {
    if (!isOpen) {
      return;
    }
    const dialog = ref.current;
    dialog.showModal();
    return () => {
      dialog.close();
    };
  }, [isOpen]);

  return <dialog ref={ref}>{children}</dialog>;
}
body {
  min-height: 300px;
}

要素の可視性の追跡 {/tracking-element-visibility/}

この例では、外部システムは再びブラウザの DOM です。App コンポーネントは長いリストを表示し、その後に Box コンポーネントを表示し、もう一度長いリストを表示します。リストを下にスクロールしてみてください。Box コンポーネントの全体が完全にビューポート内に表示されると、背景色が黒に変わることに気付くでしょう。これを実装するために、Box コンポーネントはエフェクトを使用して IntersectionObserver を管理しています。このブラウザ API は、DOM 要素がビューポートに表示されているときに通知してくれるものです。

import Box from './Box.js';

export default function App() {
  return (
    <>
      <LongSection />
      <Box />
      <LongSection />
      <Box />
      <LongSection />
    </>
  );
}

function LongSection() {
  const items = [];
  for (let i = 0; i < 50; i++) {
    items.push(<li key={i}>Item #{i} (keep scrolling)</li>);
  }
  return <ul>{items}</ul>
}
import { useRef, useEffect } from 'react';

export default function Box() {
  const ref = useRef(null);

  useEffect(() => {
    const div = ref.current;
    const observer = new IntersectionObserver(entries => {
      const entry = entries[0];
      if (entry.isIntersecting) {
        document.body.style.backgroundColor = 'black';
        document.body.style.color = 'white';
      } else {
        document.body.style.backgroundColor = 'white';
        document.body.style.color = 'black';
      }
    }, {
       threshold: 1.0
    });
    observer.observe(div);
    return () => {
      observer.disconnect();
    }
  }, []);

  return (
    <div ref={ref} style={{
      margin: 20,
      height: 100,
      width: 100,
      border: '2px solid black',
      backgroundColor: 'blue'
    }} />
  );
}

カスタムフックにエフェクトをラップする {/wrapping-effects-in-custom-hooks/}

エフェクトは「避難ハッチ」です。React の外に出る必要があり、かつ特定のユースケースに対してより良い組み込みのソリューションがない場合に使用します。エフェクトを手で何度も書く必要があることに気付いたら、通常それは、あなたのコンポーネントが依存する共通の振る舞いのためのカスタムフックを抽出する必要があるというサインです。

例えば、この useChatRoom カスタムフックは、エフェクトのロジックをより宣言的な API の背後に「隠蔽」します。

function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);
}

この後で、任意のコンポーネントから以下のように使うことができます。

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });
  // ...

ほかにも React のエコシステムには、さまざまな目的のための優れたカスタムフックが多数公開されています。

カスタムフックでエフェクトをラップする方法についてもっと学ぶ

カスタム useChatRoom フック {/custom-usechatroom-hook/}

この例は、これまでの例 のいずれかと同じですが、カスタムフックにロジックが抽出されています。

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}
import { useEffect } from 'react';
import { createConnection } from './chat.js';

export function useChatRoom({ serverUrl, roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]);
}
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }

カスタム useWindowListener フック {/custom-usewindowlistener-hook/}

この例は、前の例の中の 1 つと同じですが、ロジックがカスタムフックに抽出されています。

import { useState } from 'react';
import { useWindowListener } from './useWindowListener.js';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useWindowListener('pointermove', (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  });

  return (
    <div style={{
      position: 'absolute',
      backgroundColor: 'pink',
      borderRadius: '50%',
      opacity: 0.6,
      transform: `translate(${position.x}px, ${position.y}px)`,
      pointerEvents: 'none',
      left: -20,
      top: -20,
      width: 40,
      height: 40,
    }} />
  );
}
import { useState, useEffect } from 'react';

export function useWindowListener(eventType, listener) {
  useEffect(() => {
    window.addEventListener(eventType, listener);
    return () => {
      window.removeEventListener(eventType, listener);
    };
  }, [eventType, listener]);
}
body {
  min-height: 300px;
}

カスタム useIntersectionObserver フック {/custom-useintersectionobserver-hook/}

この例は、前の例の中の 1 つと同じですが、ロジックが部分的にカスタムフックに抽出されています。

import Box from './Box.js';

export default function App() {
  return (
    <>
      <LongSection />
      <Box />
      <LongSection />
      <Box />
      <LongSection />
    </>
  );
}

function LongSection() {
  const items = [];
  for (let i = 0; i < 50; i++) {
    items.push(<li key={i}>Item #{i} (keep scrolling)</li>);
  }
  return <ul>{items}</ul>
}
import { useRef, useEffect } from 'react';
import { useIntersectionObserver } from './useIntersectionObserver.js';

export default function Box() {
  const ref = useRef(null);
  const isIntersecting = useIntersectionObserver(ref);

  useEffect(() => {
   if (isIntersecting) {
      document.body.style.backgroundColor = 'black';
      document.body.style.color = 'white';
    } else {
      document.body.style.backgroundColor = 'white';
      document.body.style.color = 'black';
    }
  }, [isIntersecting]);

  return (
    <div ref={ref} style={{
      margin: 20,
      height: 100,
      width: 100,
      border: '2px solid black',
      backgroundColor: 'blue'
    }} />
  );
}
import { useState, useEffect } from 'react';

export function useIntersectionObserver(ref) {
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    const div = ref.current;
    const observer = new IntersectionObserver(entries => {
      const entry = entries[0];
      setIsIntersecting(entry.isIntersecting);
    }, {
       threshold: 1.0
    });
    observer.observe(div);
    return () => {
      observer.disconnect();
    }
  }, [ref]);

  return isIntersecting;
}

非 React ウィジェットの制御 {/controlling-a-non-react-widget/}

外部システムをあなたのコンポーネントの props や state に同期させたいことがあります。

例えば、React を使っていないサードパーティ製のマップウィジェットやビデオプレーヤコンポーネントがある場合、エフェクトを使ってそちらのメソッドを呼び出し、そちらの状態を React コンポーネントの現在 state に合わせることができます。以下では、map-widget.js に定義された MapWidget クラスのインスタンスをエフェクトが作成します。Map コンポーネントの props である zoomLevel が変更されると、エフェクトがクラスインスタンスの setZoom() を呼び出して、同期を保ちます。

{
  "dependencies": {
    "leaflet": "1.9.1",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "remarkable": "2.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
import { useState } from 'react';
import Map from './Map.js';

export default function App() {
  const [zoomLevel, setZoomLevel] = useState(0);
  return (
    <>
      Zoom level: {zoomLevel}x
      <button onClick={() => setZoomLevel(zoomLevel + 1)}>+</button>
      <button onClick={() => setZoomLevel(zoomLevel - 1)}>-</button>
      <hr />
      <Map zoomLevel={zoomLevel} />
    </>
  );
}
import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}
import 'leaflet/dist/leaflet.css';
import * as L from 'leaflet';

export class MapWidget {
  constructor(domNode) {
    this.map = L.map(domNode, {
      zoomControl: false,
      doubleClickZoom: false,
      boxZoom: false,
      keyboard: false,
      scrollWheelZoom: false,
      zoomAnimation: false,
      touchZoom: false,
      zoomSnap: 0.1
    });
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
      attribution: '© OpenStreetMap'
    }).addTo(this.map);
    this.map.setView([0, 0], 0);
  }
  setZoom(level) {
    this.map.setZoom(level);
  }
}
button { margin: 5px; }

この例では、クリーンアップ関数は必要ありません。なぜなら、MapWidget クラスは自身に渡された DOM ノードのみを管理しているためです。React の Map コンポーネントがツリーから削除された後、DOM ノードと MapWidget クラスインスタンスは、ブラウザの JavaScript エンジンによって自動的にガベージコレクションされます。


エフェクトを使ったデータフェッチ {/fetching-data-with-effects/}

エフェクトを使って、コンポーネントに必要なデータをフェッチ(fetch, 取得)することができます。ただしフレームワークを使用している場合は、エフェクトを自力で記述するよりも、フレームワークのデータフェッチメカニズムを使用する方がはるかに効率的であることに注意してください。

エフェクトを使って自力でデータをフェッチしたい場合は、以下のようなコードを書くことになります。

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);

  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    };
  }, [person]);

  // ...

ignore 変数に注目してください。これは false で初期化され、クリーンアップ時に true に設定されます。これにより、コードが "競合状態 (race condition)" に悩まされないようになります。ネットワークレスポンスは、送信した順序と異なる順序で届くことがあることに注意しましょう。

{/* TODO(@poteto) - investigate potential false positives in react compiler validation */}

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}
export async function fetchBio(person) {
  const delay = person === 'Bob' ? 2000 : 200;
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('This is ' + person + '’s bio.');
    }, delay);
  })
}

また、async / await 構文を使って書き直すこともできますが、この場合でもクリーンアップ関数を渡す必要があります。

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}
export async function fetchBio(person) {
  const delay = person === 'Bob' ? 2000 : 200;
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('This is ' + person + '’s bio.');
    }, delay);
  })
}

エフェクト内で直接データフェッチを書くとコードの繰り返しが増え、キャッシュやサーバレンダリングといった最適化を後から追加することが難しくなります。独自の、あるいはコミュニティがメンテナンスしているカスタムフックを使う方が簡単です

エフェクトでのデータ取得に代わる良い方法は? {/what-are-good-alternatives-to-data-fetching-in-effects/}

特に完全にクライアントサイドのアプリにおいては、エフェクトの中で fetch コールを書くことはデータフェッチの一般的な方法です。しかし、これは非常に手作業頼りのアプローチであり、大きな欠点があります。

  • エフェクトはサーバ上では動作しません。これは、サーバレンダリングされた初期 HTML にはデータのないローディング中という表示のみが含まれてしまうことを意味します。クライアントのコンピュータは、すべての JavaScript をダウンロードし、アプリをレンダーした後になってやっと、今度はデータを読み込む必要もあるということに気付くことになります。これはあまり効率的ではありません。
  • エフェクトで直接データフェッチを行うと、「ネットワークのウォーターフォール(滝)」を作成しやすくなります。親コンポーネントをレンダーし、それが何かデータをフェッチし、それによって子コンポーネントをレンダーし、今度はそれが何かデータのフェッチを開始する、といった具合です。ネットワークがあまり速くない場合、これはすべてのデータを並行で取得するよりもかなり遅くなります。
  • エフェクト内で直接データフェッチするということはおそらくデータをプリロードもキャッシュもしていないということです。例えば、コンポーネントがアンマウントされた後に再びマウントされる場合、データを再度取得する必要があります。
  • 人にとって書きやすいコードになりません競合状態のようなバグを起こさないように fetch コールを書こうとすると、かなりのボイラープレートコードが必要です。

上記の欠点は、マウント時にデータをフェッチするのであれば、React に限らずどのライブラリを使う場合でも当てはまる内容です。ルーティングと同様、データフェッチの実装も上手にやろうとすると一筋縄ではいきません。私たちは以下のアプローチをお勧めします。

  • フレームワークを使用している場合、組み込みのデータフェッチ機構を使用してください。モダンな React フレームワークには、効率的で上記の欠点がないデータフェッチ機構が統合されています。
  • それ以外の場合は、クライアントサイドキャッシュの使用や構築を検討してください。一般的なオープンソースのソリューションには、TanStack QueryuseSWR、および React Router 6.4+ が含まれます。自分でソリューションを構築することもできます。その場合、エフェクトを内部で使用しつつ、リクエストの重複排除、レスポンスのキャッシュ、ネットワークのウォーターフォールを回避するためのロジック(データのプリロードやルーティング部へのデータ要求の巻き上げ)を追加することになります。

これらのアプローチがどちらも適合しない場合は、引き続きエフェクト内で直接データをフェッチすることができます。


リアクティブな依存配列の指定 {/specifying-reactive-dependencies/}

エフェクトの依存配列は、自分で「選ぶ」たぐいの物ではないことに注意してください。エフェクトのコードによって使用されるすべてのリアクティブな値は、依存値として宣言されなければなりません。エフェクトの依存値のリストは、周囲のコードによって決定されます。

function ChatRoom({ roomId }) { // This is a reactive value
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
    connection.connect();
    return () => connection.disconnect();
  }, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
  // ...
}

serverUrl または roomId が変更されると、エフェクトは新しい値を使用してチャットに再接続します。

リアクティブな値には、props と、コンポーネント内に直接宣言されたすべての変数および関数が含まれますroomIdserverUrl はリアクティブな値であるため、依存値のリストから削除することはできません。それらを省略しようとした場合、React 用のリンタが正しく設定されていれば、リンタはこれを修正が必要な誤りであると指摘します。

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
  // ...
}

依存配列から何かを削除するには、リンタに対し、それが依存値である理由がないことを「証明」する必要があります。例えば、serverUrl をコンポーネントの外に移動すれば、それがリアクティブな値ではなく、再レンダー時に変更されないものであることを証明できます。

const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]); // ✅ All dependencies declared
  // ...
}

これで serverUrl がリアクティブな値でなくなった(再レンダー時に変更されない)ため、依存配列に入れる必要がなくなりました。エフェクトのコードがリアクティブな値を使用していない場合、その依存配列は空 ([]) であるべきです

const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // ✅ All dependencies declared
  // ...
}

空の依存配列で定義したエフェクトは、コンポーネントの props や state が変更された場合でも再実行されません。

既存のコードベースがある場合、以下のようにしてリンタを黙らせているエフェクトを見かけるかもしれません。

useEffect(() => {
  // ...
  // 🔴 Avoid suppressing the linter like this:
  // eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

依存配列がコードと一致しない場合、バグが発生するリスクが高くなります。リンタを抑制することで、エフェクトが依存する値について React に「嘘」をつくことになります。代わりにそれらが不要であることを証明してください

依存配列を渡す {/passing-a-dependency-array/}

依存配列を指定すると、エフェクトは最初のレンダー後および依存配列が変わった後の再レンダー後に実行されます。

useEffect(() => {
  // ...
}, [a, b]); // Runs again if a or b are different

以下の例では、serverUrlroomIdリアクティブな値であるため、両方とも依存配列の中で指定する必要があります。その結果、ドロップダウンで別のルームを選択したり、サーバ URL の入力欄を編集したりすると、チャットが再接続されます。ただし、message はエフェクトで使用されていない(依存する値ではない)ため、メッセージを編集してもチャットが再接続されることはありません。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
      <label>
        Your message:{' '}
        <input value={message} onChange={e => setMessage(e.target.value)} />
      </label>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
        <button onClick={() => setShow(!show)}>
          {show ? 'Close chat' : 'Open chat'}
        </button>
      </label>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId}/>}
    </>
  );
}
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
input { margin-bottom: 10px; }
button { margin-left: 5px; }

空の依存配列を渡す {/passing-an-empty-dependency-array/}

あなたのエフェクトがリアクティブな値を本当に使っていないのであれば、それは初回のレンダー後にのみ実行されます。

useEffect(() => {
  // ...
}, []); // Does not run again (except once in development)

空の依存配列であっても、セットアップとクリーンアップは開発中には 1 回余分に実行され、バグを見つけるのに役立ちます

以下の例では、serverUrlroomId の両方がハードコードされています。コンポーネントの外側で宣言されているため、これらはリアクティブな値ではなく、従って依存配列に入れる必要もありません。依存値のリストは空なので、再レンダー時にこのエフェクトが再実行されることもありません。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';
const roomId = 'music';

function ChatRoom() {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <label>
        Your message:{' '}
        <input value={message} onChange={e => setMessage(e.target.value)} />
      </label>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

依存配列を渡さない {/passing-no-dependency-array-at-all/}

依存配列自体をまったく渡さない場合、コンポーネントの毎回のレンダー(再レンダー)後にエフェクトが実行されます。

useEffect(() => {
  // ...
}); // Always runs again

この例では、serverUrlroomId が変更されるとエフェクトが再実行され、それは理にかなっています。ただし、message が変更された場合でもやはりエフェクトは再実行され、それはおそらく望ましくありません。ですので通常は依存配列を指定します。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }); // No dependency array at all

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
      <label>
        Your message:{' '}
        <input value={message} onChange={e => setMessage(e.target.value)} />
      </label>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
        <button onClick={() => setShow(!show)}>
          {show ? 'Close chat' : 'Open chat'}
        </button>
      </label>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId}/>}
    </>
  );
}
export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
input { margin-bottom: 10px; }
button { margin-left: 5px; }

エフェクト内で以前の state に基づいて state を更新する {/updating-state-based-on-previous-state-from-an-effect/}

エフェクトから以前の state に基づいて state を更新したい場合、問題が発生するかもしれません。

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1); // You want to increment the counter every second...
    }, 1000)
    return () => clearInterval(intervalId);
  }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
  // ...
}

count はリアクティブな値なので、依存配列に指定する必要があります。ただし、このままでは count が変更されるたびに、エフェクトがクリーンアップとセットアップを繰り返すことになります。これは望ましくありません。

この問題を解決するには、setCountc => c + 1 という state 更新用関数を渡します

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Pass a state updater
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Now count is not a dependency

  return <h1>{count}</h1>;
}
label {
  display: block;
  margin-top: 20px;
  margin-bottom: 20px;
}

body {
  min-height: 150px;
}

c => c + 1count + 1 の代わりに渡すようになったので、このエフェクトはもう count に依存する必要はありません。この修正の結果、count が変化するたびにインターバルのクリーンアップとセットアップを行わなくてもよくなります。


オブジェクト型の不要な依存値を削除する {/removing-unnecessary-object-dependencies/}

エフェクトがレンダー中に作成されたオブジェクトや関数に依存している場合、必要以上にエフェクトが実行されてしまうことがあります。たとえば、このエフェクトは options オブジェクトがレンダーごとに異なるため、毎回のレンダー後に再接続を行ってしまいます:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  const options = { // 🚩 This object is created from scratch on every re-render
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options); // It's used inside the Effect
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // 🚩 As a result, these dependencies are always different on a re-render
  // ...

レンダー中に新たに作成されたオブジェクトを依存値として使用しないでください。代わりに、エフェクトの中でオブジェクトを作成します:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}
export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }

エフェクトの中で options オブジェクトを作成するようになったので、エフェクト自体は roomId 文字列にしか依存しません。

この修正により、入力フィールドに文字を入力してもチャットが再接続されることはなくなります。オブジェクトは再レンダーのたびに再作成されるのとは異なり、roomId のような文字列は別の値に設定しない限り変更されません。依存値の削除に関する詳細を読む


関数型の不要な依存値を削除する {/removing-unnecessary-function-dependencies/}

エフェクトがレンダー中に作成されたオブジェクトや関数に依存している場合、必要以上にエフェクトが実行されてしまうことがあります。たとえば、このエフェクトは createOptions 関数がレンダーごとに異なるため、毎回再接続を行ってしまいます:

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  function createOptions() { // 🚩 This function is created from scratch on every re-render
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions(); // It's used inside the Effect
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
  // ...

再レンダーのたびに新しい関数を作成すること、それ自体には問題はなく、最適化しようとする必要はありません。ただし、エフェクトの依存値としてそれを使用する場合、毎回のレンダー後にエフェクトが再実行されてしまうことになります。

レンダー中に作成された関数を依存値として使用することは避けてください。代わりに、エフェクトの内部で宣言するようにします。

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}
export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}
input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }

createOptions 関数をエフェクト内で定義するようにしたので、エフェクト自体は roomId 文字列にのみ依存することになります。この修正により、入力欄に入力してもチャットが再接続されなくなります。再レンダー時に再作成される関数とは異なり、roomId のような文字列は他の値に設定しない限り変更されません。依存値の削除について詳しくはこちら


エフェクトから最新の props と state を読み取る {/reading-the-latest-props-and-state-from-an-effect/}

デフォルトでは、エフェクトからリアクティブな値を読み取るときは、それを依存値として追加する必要があります。これにより、エフェクトはその値の変更に対して「反応」することが保証されます。ほとんどの依存値については、それが望む挙動です。

ただし、時には「反応」をせずに最新の props や state を エフェクト内から読み取りたいことがあるでしょう。例えば、ショッピングカート内のアイテム数をページ訪問ごとに記録する場合を想像してみてください。

function Page({ url, shoppingCart }) {
  useEffect(() => {
    logVisit(url, shoppingCart.length);
  }, [url, shoppingCart]); // ✅ All dependencies declared
  // ...
}

url の変更ごとに新しいページ訪問を記録したいが、shoppingCart の変更のみでは記録したくない場合はどうすればいいのでしょうか? リアクティブルールに反することなく shoppingCart を依存配列から除外することはできません。しかし、エフェクト内から呼ばれるコードの一部であるにもかかわらず、そのコードが変更に「反応」しないことを示すことができます。useEffectEvent フックを使用して、エフェクトイベント (Effect Event) を宣言し、shoppingCart を読み取るコードをその内部に移動してください。

function Page({ url, shoppingCart }) {
  const onVisit = useEffectEvent(visitedUrl => {
    logVisit(visitedUrl, shoppingCart.length)
  });

  useEffect(() => {
    onVisit(url);
  }, [url]); // ✅ All dependencies declared
  // ...
}

エフェクトイベントはリアクティブではないため、あなたのエフェクトの依存配列からは常に除く必要があります。これにより、非リアクティブなコード(最新の props や state の値を読むことができるコード)をエフェクトイベント内に入れることができます。onVisit の中で shoppingCart を読むことで、shoppingCart がエフェクトを再実行することがなくなります。

エフェクトイベントがリアクティブなコードと非リアクティブなコードをどのように分離するか詳しく読む


サーバとクライアントで異なるコンテンツを表示する {/displaying-different-content-on-the-server-and-the-client/}

アプリがサーバレンダリングを(直接ないしフレームワーク経由で)使用している場合、コンポーネントは 2 種類の環境でレンダーされます。サーバ上では、初期 HTML を生成するためにレンダーされます。クライアント上では、React がその HTML にイベントハンドラをアタッチするために再度レンダーコードを実行します。これが、ハイドレーションが動作するためには初回レンダーの出力がクライアントとサーバの両方で同一でなければならない理由です。

まれに、クライアント側で異なるコンテンツを表示する必要がある場合があります。たとえば、アプリが localStorage からデータを読み込む場合、サーバ上ではそれを行うことができません。これは以下の方法で実装できます。

{/* TODO(@poteto) - investigate potential false positives in react compiler validation */}

function MyComponent() {
  const [didMount, setDidMount] = useState(false);

  useEffect(() => {
    setDidMount(true);
  }, []);

  if (didMount) {
    // ... return client-only JSX ...
  }  else {
    // ... return initial JSX ...
  }
}

アプリがロードされている間、ユーザは初期レンダーの出力を表示します。ロードとハイドレーションが完了したら、エフェクトが実行され、didMounttrue にセットされ、再レンダーがトリガされます。これにより、クライアント専用のレンダー出力に切り替わります。エフェクトはサーバ上では実行されないため、初回サーバレンダー時には didMountfalse のままになります。

このパターンは節度を持って使用してください。遅い接続のユーザは初期コンテンツをかなり長い時間、場合によっては数秒以上表示することになります。なのでコンポーネントの見た目に違和感を与える変更をしないようにしてください。多くの場合、CSS で条件付きに異なるものを表示することで、このようなことはしなくてよくなります。


トラブルシューティング {/troubleshooting/}

コンポーネントのマウント時にエフェクトが 2 回実行される {/my-effect-runs-twice-when-the-component-mounts/}

Strict Mode がオンの場合、開発時に React は実際のセットアップの前に、セットアップとクリーンアップをもう一度実行します。

これは、エフェクトのロジックが正しく実装されていることを確認するためのストレステストです。これが目に見える問題を引き起こす場合、クリーンアップ関数に一部のロジックが欠けています。クリーンアップ関数は、セットアップ関数が行っていたことを停止ないし元に戻す必要があります。基本原則は、ユーザがセットアップが一度呼ばれた場合(本番環境の場合)と、セットアップ → クリーンアップ → セットアップというシーケンスで呼ばれた場合(開発環境の場合)で、違いを見分けられてはいけない、ということです。

どのようにバグを見つけるのに役立つか と、ロジックを修正する方法 について詳しく読む。


エフェクトが再レンダーごとに実行される {/my-effect-runs-after-every-re-render/}

まず、依存配列の指定を忘れていないか確認してください。

useEffect(() => {
  // ...
}); // 🚩 No dependency array: re-runs after every render!

依存配列を指定しているにもかかわらず、エフェクトがループで再実行される場合、それは再レンダーごとに依存する値のどれかが変わっているためです。

この問題は、手動で依存する値をコンソールにログ出力することでデバッグできます。

  useEffect(() => {
    // ..
  }, [serverUrl, roomId]);

  console.log([serverUrl, roomId]);

次に、コンソール上の異なる再レンダーから表示された配列を右クリックし、それぞれで "Store as a global variable" を選択します。最初のものが temp1 として保存され、2 番目のものが temp2 として保存されたとすると、以下のようにブラウザのコンソールを使って、両方の配列でそれぞれの値が同じかどうかを確認できます。

Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...

再レンダーごとに値の変わる依存値が見つかった場合、通常は次の方法のいずれかで修正できます。

最後の手段として、上記の方法がうまくいかなかった場合、その値を作っているところを useMemo または(関数の場合)useCallback でラップしてください。


エフェクトが無限ループで再実行され続ける {/my-effect-keeps-re-running-in-an-infinite-cycle/}

エフェクトが無限ループで実行される場合、以下の 2 つの条件が成立しているはずです。

  • エフェクトが何らかの state を更新している。
  • その state 更新により再レンダーが発生し、それによりエフェクトの依存配列が変更されている。

問題を修正する前に、エフェクトが外部システム(DOM、ネットワーク、サードパーティのウィジェットなど)に接続しているかどうかを確認してください。エフェクトが state を設定する必要がある理由は何ですか? 外部システムと同期するためですか? それとも、アプリケーションのデータフローをそれで管理しようとしているのでしょうか?

外部システムがない場合、そもそもエフェクトを削除することでロジックが簡略化されるかどうか、検討してください。

もし本当に外部システムと同期している場合は、エフェクトがいつ、どのような条件下で state を更新する必要があるか考えてみてください。何か、コンポーネントの視覚的な出力に影響を与える変更があるのでしょうか? レンダーに使用されないデータを管理する必要がある場合は、ref(再レンダーをトリガしない)の方が適切かもしれません。エフェクトが必要以上に state を更新(して再レンダーをトリガ)していないことを確認してください。

最後に、エフェクトが適切なタイミングで state を更新しているものの、それでも無限ループが残っている場合は、その state の更新によりエフェクトの依存配列のどれかが変更されているためです。依存配列の変更をデバッグする方法を確認してください


コンポーネントがアンマウントされていないのにクリーンアップロジックが実行される {/my-cleanup-logic-runs-even-though-my-component-didnt-unmount/}

クリーンアップ関数は、アンマウント時だけでなく、依存配列が変更された後の再レンダー後にも実行されます。また、開発中には、React がコンポーネントのマウント直後に、セットアップ+クリーンアップを 1 回追加で実行します。

対応するセットアップコードのないクリーンアップコードをお持ちの場合、通常はコードの問題があります。

useEffect(() => {
  // 🔴 Avoid: Cleanup logic without corresponding setup logic
  return () => {
    doSomething();
  };
}, []);

クリーンアップロジックはセットアップロジックと「対称的」であり、セットアップが行ったことを停止ないし元に戻す必要があります。

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [serverUrl, roomId]);

エフェクトのライフサイクルがコンポーネントのライフサイクルとどのように異なるかを学びましょう


エフェクトが表示に関することを行っており、実行前にちらつきが見られる {/my-effect-does-something-visual-and-i-see-a-flicker-before-it-runs/}

エフェクトがブラウザの画面描画をブロックする必要がある場合は、useEffect の代わりに useLayoutEffect を使用してください。ただし、これはほとんどのエフェクトには必要ないということに注意してください。これは、ブラウザ描画の前にエフェクトを実行することが重要な場合にのみ必要です。例えば、ユーザがツールチップを見る前に、ツールチップのサイズを測定して配置するために使用します。