イベントとエフェクトを切り離す

イベントハンドラは同じインタラクションを再度実行した場合のみ再実行されます。イベントハンドラとは異なり、エフェクトは、プロパティや state 変数のような読み取った値が、前回のレンダー時の値と異なる場合に再同期を行います。また、ある値には反応して再実行するが、他の値には反応しないエフェクトなど、両方の動作をミックスさせたい場合もあります。このページでは、その方法を説明します。

このページで学ぶこと

  • イベントハンドラとエフェクトの選択方法
  • エフェクトがリアクティブで、イベントハンドラがリアクティブでない理由
  • エフェクトのコードの一部をリアクティブにしない場合の対処法
  • エフェクトイベントとは何か、そしてエフェクトイベントからエフェクトを抽出する方法
  • エフェクトイベントを使用してエフェクトから最新の props と state を読み取る方法

イベントハンドラとエフェクトのどちらを選ぶか

まず、イベントハンドラとエフェクトの違いについておさらいしましょう。

チャットルームのコンポーネントを実装している場合を想像してください。要件は次のようなものです:

  1. コンポーネントは選択されたチャットルームに自動的に接続する
  2. 「Send」ボタンをクリックすると、チャットにメッセージが送信される

あなたはそのためのコードはすでに実装されているが、それをどこに置くか迷っているとしましょう。イベントハンドラを使うべきか、エフェクトを使うべきか。この質問に答える必要があるたびに、なぜそのコードが実行される必要があるのかを考えてみてください。

特定のインタラクションに反応して実行されるイベントハンドラ

ユーザの立場からすると、メッセージの送信は、特定の「送信」ボタンがクリックされたから起こるはずです。それ以外の時間や理由でメッセージを送信すると、ユーザはむしろ怒るでしょう。そのため、メッセージの送信はイベントハンドラで行う必要があります。イベントハンドラを使えば、特定のインタラクションを処理することができます:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>;
</>
);
}

イベントハンドラを使えば、ユーザがボタンを押したときだけ sendMessage(message) が実行されるようにすることができます。

同期が必要なときに実行されるエフェクト

また、コンポーネントをチャットルームに接続しておく必要があることを思い出してください。そのコードはどこに記述されるのでしょうか?

このコードを実行する理由は、何か特定のインタラクションではありません。ユーザがなぜ、どのようにチャットルームの画面に移動したかは問題ではありません。ユーザがチャットルームの画面を見て、対話できるようになった今、このコンポーネントは、選択されたチャットサーバに接続されたままである必要があります。チャットルーム・コンポーネントがアプリの初期画面であり、ユーザが何のインタラクションも行っていない場合でも、接続する必要があります。これがエフェクトである理由です:

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

このコードを使用すると、ユーザが行った特定のインタラクションに関係なく、現在選択されているチャットサーバへの接続が常にアクティブであることを確認することができます。ユーザがアプリを開いただけであろうと、別の部屋を選んだだけであろうと、別の画面に移動して戻ってきただけであろうと、このエフェクトはコンポーネントが現在選択されている部屋と同期していることを保証し、必要なときはいつでも再接続するようにします。

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

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

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

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

  function handleSendClick() {
    sendMessage(message);
  }

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

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} />}
    </>
  );
}

リアクティブな値とリアクティブなロジック

直感的に言うと、イベントハンドラは、例えばボタンをクリックするなど、常に「手動」でトリガされます。一方、エフェクトは「自動」であり、同期を保つために必要な回数だけ実行され、再実行されます。

もっと正確な考え方があります。

コンポーネントの body 内で宣言された props 、state 、変数をリアクティブ値と呼びます。この例では、serverUrl はリアクティブ値ではありませんが、roomIdmessage はリアクティブ値です。これらは、レンダーのデータフローに参加しています:

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

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

// ...
}

これらのようなリアクティブな値は、再レンダーによって変更される可能性があります。例えば、ユーザが message を編集したり、ドロップダウンで別の roomId を選択することがあります。イベントハンドラとエフェクトは、それぞれ異なる方法で変化に対応します:

  • **イベントハンドラ内のロジックはリアクティブではない。**ユーザが同じ操作(クリックなど)を再度行わない限り、再度実行されることはありません。イベントハンドラは、その変更に「反応」することなく、リアクティブ値を読み取ることができます。
  • **エフェクト内のロジックはリアクティブである。**エフェクトがリアクティブ値を読み取る場合、依存配列としてそれを指定する必要があります。そして、再レンダーによってその値が変更された場合、React は新しい値でエフェクトのロジックを再実行します。

この違いを説明するために、先ほどの例をもう一度見てみましょう。

イベントハンドラ内のロジックはリアクティブではない

このコードの行を見てみてください。このロジックはリアクティブであるべきでしょうか、そうではないでしょうか?

// ...
sendMessage(message);
// ...

ユーザから見れば、**message の変更は、メッセージを送りたいということではありません。**あくまでも、ユーザが入力していることを意味します。つまり、メッセージを送るロジックはリアクティブであってはならないのです。リアクティブ値が変わったからと言って、再び実行されるべきではないのです。だから、イベントハンドラの中にあるのです:

function handleSendClick() {
sendMessage(message);
}

イベントハンドラはリアクティブではないので、sendMessage(message)はユーザが送信ボタンをクリックしたときのみ実行されます。

エフェクト内のロジックはリアクティブである

では、この行に戻りましょう:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...

ユーザからすると、**roomId の変更は、別の部屋に接続したいことを意味します。**つまり、ルームに接続するためのロジックはリアクティブであるべきなのです。これらのコードは、リアクティブ値に「ついていける」ようにし、その値が異なる場合は再度実行するようにします。だから、エフェクトの中にあるのです:

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

エフェクトはリアクティブなので、createConnection(serverUrl, roomId)connection.connect() は、roomId の異なる値ごとに実行されます。エフェクトは、現在選択されているルームに同期したチャット接続を維持します。

エフェクトから非リアクティブなロジックを抽出する

リアクティブなロジックと非リアクティブなロジックを混在させる場合は、さらに厄介なことになります。

例えば、ユーザがチャットに接続したときに通知を表示したいとします。props から現在のテーマ(ダークまたはライト)を読み取り、正しい色で通知を表示することができます:

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
// ...

しかし、theme はリアクティブな値であり(再レンダーの結果として変化する可能性がある)、エフェクトが読み取るすべてのリアクティブ値は、その依存配列として宣言する必要があります。そこで、エフェクトの依存配列として theme を指定する必要があります:

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ All dependencies declared
// ...

この例で遊んでみて、このユーザエクスペリエンスの問題点を見つけることができるかどうか確認してください:

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

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

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = 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>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

roomId が変わると、期待通りチャットが再接続されます。しかし、theme も依存関係にあるため、ダークとライトを切り替えるたびに、チャットも再接続されます。これはあまり良くないですね!

つまり、この行は(リアクティブである)エフェクトの中にあるにもかかわらず、リアクティブであってほしくないということです:

// ...
showNotification('Connected!', theme);
// ...

この非リアクティブなロジックと、その周りのリアクティブエフェクトを切り離す方法が必要です。

エフェクトイベントの宣言

Under Construction

このセクションでは、まだ安定版の React でリリースされていない実験的な API について説明しています。

useEffectEvent という特別な Hook を使って、エフェクトからこの非リアクティブなロジックを抽出します:

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...

ここでは、onConnectedエフェクトイベントと呼ばれています。これはエフェクトロジックの一部ですが、イベントハンドラにより近い動作をします。この中のロジックはリアクティブではなく、常に props と state の最新の値を「見る」ことができます。

これでエフェクトの内部から onConnected エフェクトイベントを呼び出せるようになりました:

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});

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

これで問題は解決しました。なお、エフェクトの依存配列のリストから onConnected を削除する必要がありました。エフェクトイベントはリアクティブではないので、依存配列から除外する必要があります。

新しい動作が期待通りに振舞うことを確認します:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

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

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

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

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = 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>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

エフェクトイベントは、イベントハンドラと非常に似ていると考えることができます。主な違いは、イベントハンドラがユーザの操作に反応して実行されるのに対し、エフェクトイベントはエフェクトからトリガされることです。エフェクトイベントは、エフェクトのリアクティブ性と反応しないはずのコードとの間の「連鎖を断ち切る」ことができます。

エフェクトイベントで最新の props や state を取得する

Under Construction

このセクションでは、まだ安定版の React でリリースされていない実験的な API について説明しています。

エフェクトイベントによって、依存性リンタを抑制したくなるような多くのパターンを修正することができます。

例えば、ページの訪問を記録するエフェクトがあるとします:

function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}

その後、サイトに複数のルートを追加します。ここで、Page コンポーネントは現在のパスを持つ url プロパティを受け取ります。この urllogVisit 呼び出しの一部として渡したいのですが、依存性リンタが文句を言ってきます:

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect has a missing dependency: 'url'
// ...
}

コードに何をさせたいか考えてみてください。各 URL は異なるページを表しているので、異なる URL に対して別々の訪問を記録したいのです。言い換えれば、この logVisit 呼び出しは、url に関して反応的でなければなりません。このため、この場合は、依存関係リンタに従って、url を依存配列に追加することが理にかなっています:

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ 全ての依存値が宣言されています
// ...
}

ここで、ページ訪問ごとにショッピングカートの商品数を一緒に表示させたいとします:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React HookのuseEffectに依存値'numberOfItems'がありません
// ...
}

あなたはエフェクトの中で numberOfItems を使用したので、リンタは依存値としてそれを追加するように求めます。しかし、logVisit の呼び出しが numberOfItems に対してリアクティブであることを望んでいません。ユーザがショッピングカートに何かを入れて、numberOfItems が変化しても、それはユーザが再びページを訪れたことを意味しない。つまり、ページを訪れたということは、ある意味で”イベント”なのです。ある瞬間に起こるのです。

コードを 2 つに分割してみましょう:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ 全ての依存値が宣言されています
// ...
}

ここで、onVisit はエフェクトイベントです。この中のコードはリアクティブではありません。このため、numberOfItems(または他のリアクティブな値!)を使用しても、変更時に周囲のコードが再実行される心配はありません。

一方、エフェクトそのものはリアクティブなままです。エフェクトの中のコードは url プロパティを使用するので、異なる url で再レンダーするたびにエフェクトが再実行されます。その結果、onVisit エフェクトイベントが呼び出されます。

その結果、url の変更ごとに logVisit が呼び出され、常に最新の numberOfItems を読み取ることになります。ただし、numberOfItems が独自に変化しても、コードの再実行には至りません。

補足

引数なしで onVisit() を呼び出し、その中の url を読み取ることができるかどうか疑問に思うかもしれません:

const onVisit = useEffectEvent(() => {
logVisit(url, numberOfItems);
});

useEffect(() => {
onVisit();
}, [url]);

これでもいいのですが、この url を明示的にエフェクトイベントに渡す方がいいでしょう。エフェクトイベントの引数として url を渡すことで、異なる url を持つページを訪問することが、ユーザの視点から見ると別の”イベント”を構成していると伝えることになります。visitedUrl は、起こった”イベント”の一部なのです:

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]);

エフェクトイベントで visitedUrl を明示的に”要求”するので、エフェクトの依存配列から誤って url を削除することができなくなりました。もし、url の依存値を削除してしまうと(別々のページへの訪問が 1 つとしてカウントされてしまう)、リンタはそれについて警告を発します。onVisiturl に関して反応的であることを期待するので、url を内部で読み込む代わりに(反応的でない)、エフェクトからそれを渡します。

これは、エフェクトの中に非同期のロジックがある場合に特に重要になります:

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
setTimeout(() => {
onVisit(url);
}, 5000); // 訪問ログの遅延
}, [url]);

ここで、onVisit 内の url最新url(既に変更されている可能性がある)に対応し、visitedUrl はこのエフェクト(およびこの onVisit コール)を最初に実行させた url に対応しています。

さらに深く知る

代わりに依存性リンタを抑制してもいいのでしょうか?

既存のコードベースでは、このように lint ルールが抑制されているのを見かけることがあります:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 このようにリンタを抑制することは避けてください:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// ...
}

useEffectEvent が React の安定した一部となった後、決してリンタを抑制しないことをお勧めします。

ルールを抑制することの最初の欠点は、コードに導入した新しいリアクティブな依存配列にエフェクトが”反応する”必要があるときに、React が警告を発しなくなることです。先ほどの例では、依存配列に url を追加したのは、React がそれをするよう思い出させてくれたからです。リンタを無効にすると、今後そのエフェクトを編集する際に、そのようなリマインダを受け取ることができなくなります。これはバグにつながります。

以下は、リンタを抑制することで発生する紛らわしいバグの一例です。この例では、handleMove 関数は、ドットがカーソルに従うべきかどうかを決定するために、現在の canMove state 変数の値を読むことになっています。しかし、handleMove の内部では canMove は常に true です。

なぜかわかりますか?

import { useState, useEffect } from 'react';

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

  function handleMove(e) {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  }

  useEffect(() => {
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        The dot is allowed to move
      </label>
      <hr />
      <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,
      }} />
    </>
  );
}

このコードの問題は、依存性リンタを抑制することにあります。抑制を解除すると、このエフェクトは handleMove 関数に依存する必要があることがわかります。これは理にかなっています。なぜならば、handleMove はコンポーネント本体の内部で宣言されるため、リアクティブな値であることがわかります。すべてのリアクティブ値は、依存値として指定されなければなりませんが、そうでなければ時間の経過とともに陳腐化する可能性があります!

元のコードの作者は、React に対して「エフェクトはどのリアクティブ値にも依存しない([])」と “嘘”をついています。そのため、React は canMove が変更された後にエフェクトを再同期させなかったのです(handleMove に関しても)。React はエフェクトを再同期しなかったため、リスナとしてアタッチされる handleMove は、初期レンダー時に作成された handleMove 関数となります。初期レンダー時には canMovetrue であったため、初期レンダー時の handleMove は永遠にその値を見ることになります。

リンタを抑制することがなければ、陳腐化した値で問題が発生することはありません。

useEffectEvent を使えば、リンタに”嘘”をつく必要はなく、期待通りにコードが動きます:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

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

  const onMove = useEffectEvent(e => {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  });

  useEffect(() => {
    window.addEventListener('pointermove', onMove);
    return () => window.removeEventListener('pointermove', onMove);
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        The dot is allowed to move
      </label>
      <hr />
      <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,
      }} />
    </>
  );
}

これは、useEffectEvent常に正しい解決策であることを意味するものではありません。リアクティブにしたくないコード行にのみ適用する必要があります。上記のサンドボックスでは、エフェクトのコードが canMove に関して反応的であることを望んでいませんでした。そのため、エフェクトイベントを抽出することが理にかなっています。

リンタを抑制する他の正しい方法については、エフェクトの依存関係を削除するを参照してください。

エフェクトイベントの制限について

Under Construction

このセクションでは、まだ安定版の React でリリースされていない実験的な API について説明しています。

エフェクトイベントは、使い方が非常に限定されています:

  • エフェクトの内部からしか呼び出すことができません。
  • 他のコンポーネントやフックに渡してはいけません。

例えば、次のようにエフェクトイベントを宣言して渡さないでください:

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

const onTick = useEffectEvent(() => {
setCount(count + 1);
});

useTimer(onTick, 1000); // 🔴 エフェクトイベントを渡すことを避けてください

return <h1>{count}</h1>
}

function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // 依存配列で "callback" を指定する必要あり
}

その代わりに、常にエフェクトイベントを使用するエフェクトのすぐ隣で宣言してください:

function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}

function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});

useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ Good: エフェクトの内部でのみ呼び出される
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // 依存配列に "onTick" (エフェクトイベント)を指定する必要がない
}

エフェクトイベントは、エフェクトのコードの中で反応しない”ピース”です。それらを使用するエフェクトの隣に置く必要があります。

まとめ

  • イベントハンドラは、特定のインタラクションに応答して実行されます。
  • 同期が必要なときはいつでもエフェクトが実行されます。
  • イベントハンドラ内のロジックは、リアクティブではありません。
  • エフェクト内のロジックは、リアクティブです。
  • エフェクトの非リアクティブなロジックをエフェクトイベントに移動することができます。
  • エフェクトイベントを呼び出せるのはエフェクトの内部だけです。
  • エフェクトイベントを他のコンポーネントや Hooks に渡さないでください。

チャレンジ 1/4:
更新されない変数を修正する

この Timer コンポーネントは、1 秒ごとに増加する count state 変数を保持します。増加する値は、increment state 変数に格納されます。プラスボタンとマイナスボタンで increment 変数を制御できます。

しかし、プラスボタンを何度クリックしても、カウンタは 1 秒ごとに 1 つずつ増えていきます。このコードの何が問題なのでしょうか? なぜエフェクトのコード内部では increment が常に 1 に等しいのでしょうか? 間違いを見つけて修正しましょう。

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + increment);
    }, 1000);
    return () => {
      clearInterval(id);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}