はじめに
useEffectについて、以下の記事がとても分かりやすかったので、記事を読んでインプットしたものをまとめました。
他参考
useEffectの前に・・・
レンダリングについておさらいします。
それぞれの render は独自の props と state を保持している
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
このコードにおけるcountはただの数字です。
コンポーネントが一番初めに render する際、useState() から出力される count 変数は 0 です。setCount(1)を呼ぶと、React はコンポーネントは再度呼び出します。その際、 count は 1 となります。
// 初期 render 時
function Counter() {
const count = 0; // useState()の戻り値
// ...
<p>You clicked {count} times</p>;
// ...
}
// クリック後、関数が呼び出される
function Counter() {
const count = 1; // useState()の戻り値
// ...
<p>You clicked {count} times</p>;
// ...
}
// もう一度クリックすると、再度呼ばれる
function Counter() {
const count = 2; // useState()の戻り値
// ...
<p>You clicked {count} times</p>;
// ...
}
state をアップデートする度、React はコンポーネント関数を呼び出します。
それぞれの render 結果は定数として定義された counter state を見ることができます。
<p>You clicked {count} times</p>
何か特別なデータバインディングをしている訳ではなく、{count}には、結果にReactが提供した数字を組み込んでいます。
setCountするとcountの値を使ってコンポーネントが呼び出されます。
そして、render結果にマッチするようにDOMをアップデートしています。
count定数は、特定のrenderで時間の経過とともに変化するものではないです。
コンポーネント関数が呼び出されることによって、countの値を見ることができるからです。
そして各 render はそれぞれそのrenderに隔離されたcount値を見ることができます。
結果からわかること
- count は定数であり呼び出される関数ごとに保持している
それぞれの render は独自のイベントハンドラを保持している
イベントハンドラの場合はどうなるのか見てみます。
以下のコードは、3 秒後に count の値を alert するイベントです。。
function Counter() {
const [count, setCount] = useState(0);
function handleAlertClick() {
setTimeout(() => {
alert("You clicked on: " + count);
}, 3000);
}
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<button onClick={handleAlertClick}>アラートを表示</button>
</div>
);
}
下記の手順を実行したとき、アラートに表示される数字は何になるのか
countを3回まで増やす
↓
「アラート表示」を押下
↓
タイムアウトが発火する前に count を 5 まで増やす
結果
アラートは押下時の state をキャプチャーします。
結果からわかること
- 関数は何度も呼ばれる(render 毎に一度)が、その都度 count値は定数であり何かの値でセットされている(その render の state)
- 特定の render の中ではprops と state は一生変わらない。
- props と state が特定の render に隔離されていて同じ
- それを使用してる値(イベントハンドラも含む)も同様に特定の render に属している
それぞれの render は独自のエフェクトを保持している
DOMに変更が走ってブラウザに描画された後にエフェクトは実行されます。
概念的には一つのエフェクト(ドキュメントのタイトルを変える)ですが、render されるごとに 別の関数 として表されています。
そしてそれぞれのエフェクト関数は特定の render に属する props と stateを参照することができます。
概念的に、エフェクトは render 結果 の一部であると想像してもらってもいいです。
それぞれの render は全てを保持している
エフェクトは render の後に実行され、概念的にはコンポーネント出力の一部であり、特定の render 内の state と props を参照できます。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
結果
特定の render 内に属する count の値が順次出力されます。
※class コンポーネントの this.state は、このような挙動をしません。
import React, { Component } from "react";
class Example extends Component {
state = {
count: 0,
};
componentDidMount() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button
onClick={() =>
this.setState({
count: this.state.count + 1,
})
}
>
Click me
</button>
</div>
);
}
}
this.state.count は特定の render に属する値ではなく、常に最新の count の値を参照します。なので、代わりに 5 が順番に表示されます。
結果からわかること
- コンポーネント render 内の全ての関数(コンポーネント内で定義されてるイベントハンドラ、エフェクト、タイムアウト、APIの呼び出しなどを含む)は定義されてる特定の render 内の props と state をキャプチャーする
そのため、以下の2つは同じ挙動をします
function Example(props) {
useEffect(() => {
setTimeout(() => {
console.log(props.counter);
}, 1000);
});
// ...
}
function Example(props) {
const counter = props.counter;
useEffect(() => {
setTimeout(() => {
console.log(counter);
}, 1000);
});
// ...
}
重要なことは、props か state を「早めに」コンポーネント内で呼ぼうが呼ばまいが関係ないということです。
一つの render 内のスコープでは、 props と state は変わりません。
特定の render 内の値ではなく最新の値をエフェクト内で定義されてる callback内で 使いたい 場合は、useRefを利用するのが一番簡単な方法です。
useRefを使い、最新の値を参照する
function Example() {
const [count, setCount] = useState(0);
const latestCount = useRef(count);
useEffect(() => {
// mutable な最新の値をセットする
latestCount.current = count;
setTimeout(() => {
// mutable な最新の値を読む
console.log(`You clicked ${latestCount.current} times`);
}, 3000);
});
Cleanupのときはどうなるのか
下記の例
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
};
});
初期 render 時には props は {id: 10} として、2回目の render 時は {id: 20} になるとします。
この際の挙動はどうなるのでしょうか。
正解例
①React が {id: 20} の UI を render する。
②ブラウザが描画する。{id: 20} の時の UI が画面に表示される。
③React が {id: 10} のエフェクトを cleanup する。
④React が {id: 20} のエフェクトを実行する。
少し間違いです。
①React が {id: 10} のエフェクトを cleanup する。
②React が {id: 20} の UI を render する。
③React が {id: 20} のエフェクトを実行する。
Reactはブラウザの描画が終わった後に、初めてエフェクトを実行します。
前のエフェクトは新しい props で re-render されてから cleanup されます。
なぜ、前のエフェクトの cleanup は props が {id: 20} に変わって re-render された後に実行されてるのに、古い {id: 10} の props が見えてるのでしょうか。
答えは、エフェクトの cleanup はどういう意味であろうと最新の値を読んだりしないからです。 エフェクトが定義されている特定の render 内の props を読んでいるからです。
React にエフェクトを比較することを教える
function Greeting({ name }) {
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
</h1>
);
}
nameをアップデートする場合
<h1 className="Greeting">
Hello, Dan
</h1>
から
<h1 className="Greeting">
Hello, Yuzhi
</h1>
へ
Reactが受け取るオブジェクトは
const oldProps = {className: 'Greeting', children: 'Hello, Dan'};
const newProps = {className: 'Greeting', children: 'Hello, Yuzhi'};
それぞれの props を比べて、 children は変更しているので DOM アップデートは必要ですが className は変わっていないので、このような処理をします。
domNode.innerText = 'Hello, Yuzhi';
// domNode.classNameは触る必要なし
エフェクトで以下の場合はどうなるのでしょうか。
function Greeting({ name }) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = 'Hello, ' + name;
});
return (
<h1 className="Greeting">
Hello, {name}
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</h1>
);
}
この場合、countのstateは使用していないが、document.title を name prop でシンクロさせているので、countが変更されるたびに、再レンダリングされてしまいます。
- React は一度関数を呼ばないと、その関数が何をしているか推測することはできない。
- エフェクトは、単純に DOM の違いを勝手に検知できない。
そのため、不必要なエフェクトを再実行したくない場合は、依存配列(deps とも言われる)というものを第 2 引数として useEffect に渡します。
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]); // Our deps
「関数の戻り値が分からないのは知ってるけど、render scope 内で name しか使ってないことを約束するよ」と React に言ってるみたいなものです。
もし配列内のそれぞれの値が現在と一つ前のエフェクト実行時と同じであれば、シンクロするものがないので React はエフェクトをスキップします