useEffectの依存配列は自分で決めていいの?
だめです。絶対にやらないでください。
少し前の僕:「このset関数、このステートが変更された時に実行したいなぁ。依存配列をこうしてっと。ん?なんかlintのerrorがでてる。コメントアウトしたら消えるらしい。これでよし!」
すべてだめです。
ただ、Reactに携わっている方なら、一度は依存配列の扱いに悩んだことがあると思います。
少し前の僕:「じゃあ、どうすればいいの?」
そうですね、それでは順を追って説明していきます。
レンダリングについて理解しよう
少し前の僕:「コンポーネント内の関数がいつ実行されるかわからんから、useEffect
で制御してるんだよ!」
大丈夫です。わかるようになります。
まず、Reactにおけるレンダリングとは、Reactがコンポーネントを呼び出すことを指します。
呼び出されたコンポーネント内で定義されている関数や変数は、特定のケースを除き(後ほど説明します)、実行されます。
では、Reactがコンポーネントを呼び出すタイミングはいつかというと、以下の3つのケースに限られます。
- 初回レンダリング
-
state
の更新(set
関数の実行) - 親コンポーネントのレンダリング
つまり、初回レンダリングと set
関数の実行タイミング(子コンポーネントの場合、親コンポーネントの set
関数の実行タイミングも含む)を把握することで、コンポーネントがどのタイミングで実行されるかを理解できます。
具体例をみてみましょう。
この Form
コンポーネントは、firstName
と lastName
の2つのstate
で状態を管理しています。
そして、useEffect
を使って、firstName
または lastName
が更新されるたびにfullName
も更新されるようにしています。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
ただ、firstName
と lastName
の値が変更されるのは set
関数が実行されたときなので、実は useEffect
を使わなくても、単に fullName
を firstName + ' ' + lastName
として定義しておけば、自動的に更新されます。
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// firstName,lastNameの値が変更されるときはset関数が実行された時
// レンダリングが発生するたびにfullNameの値も更新されるので、useEffectは不要!
const fullName = firstName + ' ' + lastName;
// ...
}
そのため、「state
や props
が変更されたときに関数を実行したい!」という場合は、基本的に useEffect
を使わずに制御できます。
ちなみにprops
が変更されたらレンダリングされるというのはあまり正しくないです。
なぜなら、props
を変更するには結局のところ親コンポーネントをレンダリングする必要があるからです。
さらに、memo
化をしていない場合、props
の変更の有無に関わらず、レンダリングは実行されます。
そのため、memo
化をしている場合にのみ、props
の変更がレンダリングに影響を与えることになります。
副作用とは何かを理解しよう
少し前の僕:「なるほど、じゃあこのstate
が更新された時にこのfetch
でデータ取得をしたいから、そのままコンポーネント内に書いてっと、、」
だめです。副作用です。
少し前の僕:「副作用ってなんやねん!」
そうですね、それでは次に副作用について説明していきます。
まず、プログラミングにおける副作用がない関数とは、「計算内容が引数にのみ依存し、その結果は戻り値にのみ影響する」ことです。
また、副作用がない関数には以下のことが成立します。
- 同じ入力に対して常に同じ結果が得られる(冪等である)
- 他のいかなる機能(ローカル変数以外)の結果にも影響を与えない
function double(number) {
return 2 * number;
}
この関数では、
- numberが1であれば2
- numberが2であれば4
- numberが3であれば6
を常に返します。
このように同じ入力に対して常に同じ結果が得られる関数は、冪等であると言えます。
これに対して、副作用がある関数の条件は次の通りです。(副作用は広い意味で使われているので、これ以外にも該当ケースがあるかもしれません。)
- 外部の状態に依存して冪等ではない。
- 外部の状態(ローカル変数以外)を変化させる。
例えば fetch
でデータを取得・更新するような操作がこれに該当し、こうした要因を「副作用」と呼びます。
let multiplier = 2;
function double(number) {
// グローバル変数のmultiplierが関数呼び出しごとにインクリメントされている = 副作用!
multiplier++;
return number * multiplier;
}
この関数では、
- numberが1であれば3
- 再びnumberを1にしたら4
- さらに再びnumberを1にしたら5
を返します。
このように、同じ入力に対して異なる結果が返るため、この関数は冪等ではありません。
それでは次に冪等ではないコンポーネントの例を見てみましょう。
function Clock() {
const time = new Date();
return <span>{time.toLocaleString()}</span>
}
コンポーネント内では Date
をインスタンス化していますが、
これではClockコンポーネントを呼び出すたびに日付(コンポーネントの結果)が変わるため、冪等ではありません。
「これでもコンポーネントは動作しそうだけど?」と思うかもしれませんが、コンポーネントは純粋に保つべき(副作用がない状態)と定めらています。
useEffectを使用するタイミングについて理解しよう
少し前の僕:「じゃあ、さっきのnew Date()
やfetch
などの副作用をReactでは使うなってことか!?」
違います。ちゃんと使えます。
まず前述の通り、コンポーネントは純粋に保つ必要があるため、レンダリング中に副作用を含めることはできません。
ただし、裏を返せば、レンダリング中でさえなければ副作用を扱うことが可能です。
そのため、副作用を扱う場所として、イベントハンドラーが適しています。
Reactでは、副作用を基本的にイベントハンドラー内で処理するよう推奨されています。
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>
</>
);
}
ただし、副作用を処理する適切なイベントハンドラーがない場合もあります。
例えば、『初回レンダリング直後に fetch を呼び出したい』といったケースです。
このような場合、初回レンダリング後に発火するイベントトリガーは存在しません。
そこで登場するのが useEffect
です。
useEffect
はレンダリング直後に実行されるいわばイベントハンドラーみたいなものです(厳密にはレンダリング開始前にもクリーンアップ関数がよばれるわけですが、説明を省きます)。
依存配列について理解しよう
少し前の僕:「なんとなく理解したで!さて、初回レンダリング時に呼びたいからこうしてと、、ん?なんかlintのerrorがでてる。コメントアウトしt」
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []);
}
だからだめです。依存配列は絶対lintに従ってください。
そもそもuseEffect
はレンダー直後に呼ばれるイベントハンドラーみたいなものです。
なので、本来であればレンダリング直後に毎回再同期が行われます。
ただ、毎回再同期しても結果が変わらなければ無駄なので、レンダリング中に値が変更されたときのみ再同期されるようになっています。
ここでいう『値』とは、レンダリングによって変化する可能性があるstate
やprops
などのリアクティブな値のことです。
もしリアクティブな値を含めなければ、ReactとuseEffect内のロジックに差分が生じ、これはバグの要因になり得ます。
先ほどのコンポーネントで言うと、roomId
は props なので、レンダリングによって値が変わる可能性があります。
そのため、必ず roomId
を依存配列に含めなければなりません。
もし含めなければ、useEffect
内の roomId
と現在の roomId
に差分が発生します。
一方、serverUrl
はコンポーネント外で宣言されているため、依存配列には含めません。serverUrl
はレンダリング中に変わることがないからです。
// コンポーネント外なので依存配列には含めない
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// roomIdはリアクティブな値なので変更を検知するたびに再同期
}, [roomId]);
}
長々と説明してきましたが、依存配列は自分で勝手に決めるものではなく、あくまでもReactとuseEffect間における必要最低限の再同期のために存在しているということを覚えておいてください。
少し前の僕:「でも、依存配列に従ったら意図した挙動にならないんだけど。。。」
依存配列を操作する方法以外でなんとかしてください。
少し前の僕:「えぇ、、」
ケースバイケースなので、絶対にこれをしなければならないという決まりはありません(もしあれば教えてください!)。
ただし、公式ドキュメントには、解決のための有益なヒントが記載されています。
終わりに
少し前の僕:「なんとかしてくださいって投げやりすぎん?」
だってあなたエンジニアでしょ
少し前の僕:「はい。。」
ものすごく参考にした資料
Discussion