useEffect を使って「初回以外の再レンダリング時に実行される処理」を書くにはどうすれば良いのか?
という疑問を、たまに目にします。
…
前回の記事では、「useEffect ではなくイベントハンドラに処理を移動することで、《どのユーザーイベントに反応して、そのステートの遷移を引き起こすのか》を明確にする」グッドプラクティスについて説明しましたが、
これだけでは「エフェクトが初回に実行されてほしくない…」と困るケースの全てがカバーしきれていないと思います。
この記事では、もう一つのケース、「エフェクトが初回に実行されてほしくない」けど、《ステートと外部の同期》という本来の用途に当てはまるケースについて解説します。
本題: ステートによって実行する/スキップを制御する
イベントハンドラに移すような処理ではないけど、「初回レンダリング(マウント)時には実行せず、再レンダリング以降にのみ実行される」という仕様を実現したい…
という場合は、前回の記事でも出てきた《コンポーネントのステートに着目する》観点で、仕様そのものを見直してみましょう。
- ルームID 入力欄に入力するたびに、その roomId に対応するチャットルームに接続しなおす
- ただし、初回レンダリング時には呼ばない
こちらは React 公式ドキュメントでの題材をアレンジしたものです。
「初回レンダリング時には呼ばない」とありますが、「roomId が空文字のとき」に言い換えられるのではないでしょうか?
- ルームID 入力欄に入力するたびに、その roomId に対応するチャットルームに接続しなおす
- ただし、
初回レンダリング時roomId
が空文字のとき には呼ばない
「いま、このエフェクト実行は、初回レンダリングか、それ以降のレンダリングなのか」を知ることはできませんが、
「コンポーネントのステートたちが、このようなときには実行する、あのようなきには実行しない」というふうに条件を整理することで、エフェクトを使った実装が可能になりました。
export const App: FC = () => {
const [roomId, setRoomId] = useState("");
useEffect(() => {
if(!roomId) return; // roomId が空のときは実行しない
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return (
<div>
<div>roomId: {roomId}</div>
<div>
<input
type="text"
value={roomId}
onChange={(e) => setRoomId(e.target.value)}
/>
</div>
</div>
);
}
初期値にさまざま可能性がある場合
仕様が変わりました。 roomId の初期値が空でない文字になることが場合によってありえます。
今のままだと「初回だけど実行されてしまう」のでは?
export const Chatting: FC<Props> = ({ defaultRoomId = "" }) => {
const [roomId, setRoomId] = useState(defaultRoomId);
このようなコードで書くやつですね。
むしろ、そのような仕様であれば、初回に実行されるほうが正しいのではないかと思いますが…
どうしても初回だけ特別に実行を阻止したいなら、ステートを導入すると可能です。
export const Chatting: FC<Props> = ({ defaultRoomId = "" }) => {
const [roomId, setRoomId] = useState(defaultRoomId);
// createConnection の実行制御に使う。
// 入力欄への入力が一度でもあったら true に倒す。
const [shouldConnect, setShouldConnect] = useState(false);
useEffect(() => {
if(!roomId) return;
if(!shouldConnect) return;
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [shouldConnect, roomId]);
// 中略
<input
type="text"
value={roomId}
onChange={(e) => {
setRoomId(e.target.value);
setShouldConnect(true)
}}
/>
余談
今回のケースは React のエフェクトの特色が生きていると言えそうです。
エフェクトの関数の中でステートの値を読み取る(クロージャの用語を使うと「キャプチャする」)ように書いた場合、エフェクトは《スナップショットとして振る舞うステート》と上手く連携できる仕組みになっています。