どうも、ノマドクリエイターのショウヘイ(@shohei_creator)です。
ブログアシスタントのふーちゃんです。
React を使ってアプリを作っていると、「 state や props に変更があった時に、毎回 特定の関数を実行させたい」という場面に遭遇するでしょう。たとえば、『入力フォームの内容を取得して、リアルタイムで別領域にデータを表示させたい』などです。
React には、 state や props などの変化があった時に、任意の処理を実行させられる useEffect フックという機能が用意されています。
この記事では、 React の useEffect フックの構文と具体例について説明します。
React の useEffect フックの概要と実行タイミング
React には、ライフサイクルという概念があります。このライフサイクルは、 React によって DOM ノード(いわゆるコンポーネント)が生成されてから破壊されるまでの各段階によって構成されています。
React のライフサイクルでは、 DOM ノード が生成されて DOM ツリーに追加された状態をマウントした( componentDidMount )と呼びます。平たくいえば、ブラウザ上の描写に変更が起きた状態です。
「マウントした」時点をキッカケにして、特定の処理を実行できる機能こそ、 useEffect フックです。
下の引用は、 React のライフサイクルを図にしたものです。
「マウント時」列の最も下にある「 componentDidMount 」のところで、 useEffet フックが起動しますよ!
React lifecycle methods diagram
React の useEffect フックの基本構文
React の useEffect フックは、コンポーネントの中で使います。また、使う時には、 2 つの引数を設定します。
useEffect フックの第 1 引数には、実行したい関数を設定します。関数の書き方は、アロー関数でも構いません。 useEffect フックの第 2 引数には、 useEffect フックの起動を制御するために監視したい変数を要素として持つ配列を設定します。
こちらは、 JavaScript で useEffect を書いた場合のコードです。ちなみに、 TypeScript で書いた場合でも、基本構文は大差ないです。
import React, { useEffect } from "react";
export const Component = () => {
useEffect(
// 実行したい関数
() => {
(省略)
},
// 変更を監視したい変数を要素に持つ配列
[var1, var2, var3]
);
(以下、省略)
};
実際に useEffect フックを使う時は、 react ライブラリから import することを忘れないでください。
import React, { useEffect } from "react";
React の useEffect フックの第 2 引数の役割
React は、 state や props の変更が起きるたびに、マウント処理を実行します。 DOM の上書き対象のコンポーネントの中で useEffect が使われていた場合は、マウントのたびに useEffect フックも起動することになります。これでは、必要以上に useEffect が起動してしまって不便です。
そこで、 useEffect フックの第 2 引数に設定する配列の出番です。 第 2 引数の配列の要素として変数を設定すると、その変数に変更があった場合にのみ、 useEffect フックが起動する(第 1 引数の関数が実行される)ようになります。
useEffect フックの第 2 引数の配列を適切に設定することで、想定外の挙動を防げますね!
第 2 引数の配列は、必ずしも要素を持たせる必要はないんだ。
場合によっては、空の配列のまま設定してもいい。
どんな場面で使い分ければいいですか?
じゃあ、配列に『要素を持たせる場合』と『要素を持たせない場合』の具体例を載せてみようか。
React の useEffect フックを特定の変数の更新時に起動(第 2 引数の配列に要素を持たせる)
React の useEffect フックの第 2 引数の配列が要素を持っていると、その要素である変数に変更があった場合にのみ、 useEffect フックが起動するようになります。
具体例として、 2 つのカウンターを作りました。カウンターのボタンをクリックすると、 count 変数の値が更新されて、 useEffect フックが起動します。そして、コンソールに「カウンター●のボタンがクリックされました」という文章を出力する仕様です。
たとえば、カウンター 1 のボタンをクリックしても、カウンター 2 の useEffect フック内にある console.log は実行されません。カウンター 2 の useEffect フックの第 2 引数( count2 )は同じままなので、 useEffect フックが起動しないからです。
import React, { useState, useEffect } from "react";
import { Container, Row, Alert, Button } from "react-bootstrap";
export const Counters: React.VFC = () => {
const [count1, setCount1] = useState<number>(0);
const [count2, setCount2] = useState<number>(0);
useEffect(() => {
console.log("カウンター1のボタンがクリックされました");
}, [count1]);
useEffect(() => {
console.log("カウンター2のボタンがクリックされました");
}, [count2]);
return (
<Container className="mt-4">
<Row>
<Alert variant="primary">
<p>カウンター1: {count1}</p>
<Button
variant="primary"
onClick={() => setCount1((prevCount) => prevCount + 1)}
>
カウントを増やす
</Button>
</Alert>
</Row>
<Row>
<Alert variant="success">
<p>カウンター2: {count2}</p>
<Button
variant="success"
onClick={() => setCount2((prevCount) => prevCount + 1)}
>
カウントを増やす
</Button>
</Alert>
</Row>
</Container>
);
};
React の useEffect フックを初回だけ起動(第 2 引数を空配列にする)
React の useEffect フックの第 2 引数を空配列にした場合は、はじめてブラウザ表示される(初回マウント)時のみ、 useEffect フックが起動します。その後は、 state や props などが更新されて再マウントされることがあっても、 useEffect フックは起動しません。
具体例として、ボタンをクリックするたびに数値が増えるカウンターを作りました。 useState フックによって state が更新されることで、カウンターの数値が増えます。 useEffect フックの関数には、 コンソールに「マウントされました」と出力する ための console.log メソッドを用意しています。
useEffect フックの第 2 引数は、空配列になっています。はじめてブラウザ表示された時は、 コンソールに「マウントされました」と出力されます。その後は、カウンターの数値が増える(再マウントされる)ことがあっても、 useEffect フックが起動しないことが確認できます。
import React, { useState, useEffect } from "react";
import { Container, Alert, Button } from "react-bootstrap";
export const Counter: React.VFC = () => {
const [count, setCount] = useState<number>(0);
useEffect(() => {
console.log("マウントされました");
}, []);
return (
<Container className="mt-4">
<Alert variant="primary">
<p>カウンター: {count}</p>
<Button
variant="primary"
onClick={() => setCount((prevCount) => prevCount + 1)}
>
カウントを増やす
</Button>
</Alert>
</Container>
);
};
React の useEffect フックで無限ループが起きる原因と対処法
React の useEffect フックは、第2引数に設定した配列によって、マウント時に useEffect フックを起動させるかどうか制御しています。この仕様により、 useEffect フックの第 2 引数を未設定のままにしておくと、マウント時に無条件で useEffect フックが起動します。
useEffect フックの第 2 引数を未設定にした状態で、かつ第 1 引数に設定する関数に『 state や props を操作する処理』が書かれていると、無限ループによる多重レンダリングが発生することがあります。
useEffect フックによって state や props などが更新されると、 React は再マウント処理を実行します。再マウントに連動して、また useEffect フックが起動します。さらに state や props などが更新されるので、また React が再マウント処理を実行して、連動して useEffect フックが起動します。
上記のように、延々と useEffect フックが起動し続ける無限ループとなり、多重レンダリングが発生します。
プログラミング関連で「無限ループ」という言葉を聞くと、ぞっとしますね……。
state や props に対して副作用のある処理……たとえば変数に 1 を足す加算処理の場合は、無限ループが起きちゃうね。
CodeSandbox で多重レンダリングを引き起こすコードを書いて、記事に埋めこむわけにはいかないので、コードだけ載せておくよ。
import React, { useState, useEffect } from "react";
export const Counter: React.VFC = () => {
const [count, setCount] = useState<number>(0);
// 第1引数のアロー関数でcountを更新しつつ、第2引数を未設定にする
// useEffectの起動 → stateの更新 → マウント → useEffectの起動……の無限ループ
useEffect(() => {
setCount((prevCount) => prevCount + 1);
});
return (
<>
<p>カウンター: {count}</p>
</>
);
};
無限ループを回避する対処法ってありますか?
useEffect フックの第 2 引数に、何かしらの配列を設定することを忘れないことだね。
React 向けの Linter ライブラリを導入しておくと、まずい構文があると教えてくれるから、気づきやすくなるよ。
React の useEffect フックと return 文によるクリーンアップ処理
React の useEffect フックは、第 1 引数の関数に return 文が書かれていると、特殊な挙動になります。マウントする時に useEffect フックが起動しても、 return 文は実行されません。ただし、アンマウントする時になると、 retrun 文だけが実行されます。
React の useEffect フックの return 文に対する挙動は、何かしらの状態を一掃したいクリーンアップ処理を実行したい時に便利です。
useEffect フックと return 文によるクリーンアップ処理の実例を紹介しますね。
addEventListener の多重起動の防止
React は、差分検知を駆使した DOM 操作によって、ブラウザ上のコンポーネントを更新しています。つまり、ページ更新せずに、単一ページによって Web アプリケーションを構成しています。このような Web アプリケーションを SPA (シングル・ページ・アプリケーション)と呼びます。
React は SPA なので、 useEffect フックで addEventListener メソッドを使う場合には、マウントするごとに addEventListener メソッドの重ねがけが起きます。 addEventListener メソッドの重ねがけは、ブラウザのパフォーマンスを落とす原因になります。
useEffect フックによる addEventListener メソッドの重ねがけを体感してもらうために、具体例を用意しました。
現実的な実装ではないですが、 addEventListener メソッドの重ねがけが分かりやすいです。
ボタンを押すごとに、倍々のような感じで数字が増えますね……。
import React, { useState, useEffect, useRef } from "react";
import { Container, Button } from "react-bootstrap";
export const Counter: React.VFC = () => {
const [count, setCount] = useState<number>(0);
const btnRef = useRef<HTMLButtonElement | null>(null);
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
// useEffectが起動するたびに、ボタンに新たなaddEventListenerが適用される
useEffect(() => {
const btnRefCurrent = btnRef.current;
if (btnRefCurrent) {
btnRefCurrent.addEventListener("click", increment);
}
}, [count]);
return (
<Container className="mt-4">
<p>カウンター: {count}</p>
<Button variant="primary" ref={btnRef}>
カウンターの数字を増やす
</Button>
</Container>
);
};
今度は、 useEffect フックで実行する関数に、 return 文として removeEventListener メソッドを追記しました。
アンマウントされる時に return 文が実行されて、 addEventListener メソッドの内容を打ち消してくれます。
今度は、ボタンをクリックするたびに、数字が 1 つずつ増えるようになりましたね。
import React, { useState, useEffect, useRef } from "react";
import { Container, Button } from "react-bootstrap";
export const Counter: React.VFC = () => {
const [count, setCount] = useState<number>(0);
const btnRef = useRef<HTMLButtonElement | null>(null);
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
useEffect(() => {
const btnRefCurrent = btnRef.current;
if (btnRefCurrent) {
btnRefCurrent.addEventListener("click", increment);
}
// retrun文として、removeEventListenerを追加
return () => {
if (btnRefCurrent) {
btnRefCurrent.removeEventListener("click", increment);
}
};
}, [count]);
return (
<Container className="mt-4">
<p>カウンター: {count}</p>
<Button variant="primary" ref={btnRef}>
カウンターの数字を増やす
</Button>
</Container>
);
};
React の useEffect フックで非同期処理( async )する場合の注意点
Web アプリケーションを作る時には、 API を利用して、サーバーのデータベースからデータを取得する必要に迫られることがあります。初回マウント時にだけデータ取得すればいい実装なら、 useEffect フックの中に非同期処理を書きたくなるでしょう。
しかし、 useEffect フックで非同期処理を書くと、想定した通りに動かなくなることがあります。原因は、 useEffect フックの第 1 関数にそのまま非同期処理を書いてしまい、 Promise 型の返り値がクリーンアップ処理として認識されるからです。
実際のコードを見てもらった方が話が早いですね。まずは、悪い例から。
useEffect フックの第 1 引数の関数で、 async をつけた関数をそのまま実行した場合。
う~ん……データが取得できていないです。
async のついた関数の返り値は Promise 型になので、 useEffect フックが正常に機能しなくなるんですね。
import React, { useState, useEffect } from "react";
import { Container } from "react-bootstrap";
type User = {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
};
export const Component: React.VFC = () => {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
// 返り値がPromise型なので、クリーンアップ処理と誤認されてしまう
async () => {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data = await res.json();
setUsers(data);
} catch (err) {
console.error(err);
}
};
}, []);
return (
<Container className="mt-4">
{users.length ? (
users.map((user) => (
<ul key={user.id}>
<li>ID: {user.id}</li>
<li>名前: {user.name}</li>
<li>メールアドレス: {user.email}</li>
</ul>
))
) : (
<p>データが取得できていません</p>
)}
</Container>
);
};
次は、 async のついた関数を適当な変数に入れておいて、あとから関数を実行した場合。
useEffecr フックの第 1 引数の関数に、直接 return されなくなります。
ちゃんと動きましたね!
API でデータを取得できています。
import React, { useState, useEffect } from "react";
import { Container } from "react-bootstrap";
type User = {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
suite: string;
city: string;
zipcode: string;
geo: {
lat: string;
lng: string;
};
};
phone: string;
website: string;
company: {
name: string;
catchPhrase: string;
bs: string;
};
};
export const Component: React.VFC = () => {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
const getUsers = async () => {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data = await res.json();
setUsers(data);
} catch (err) {
console.error(err);
}
};
getUsers();
}, []);
return (
<Container className="mt-4">
{users.length ? (
users.map((user) => (
<ul key={user.id}>
<li>ID: {user.id}</li>
<li>名前: {user.name}</li>
<li>メールアドレス: {user.email}</li>
</ul>
))
) : (
<p>データが取得できていません</p>
)}
</Container>
);
};
先ほどは名前付き関数として実行しましたが、以下のように、 async な無名関数として実行しても OK です。
useEffect(() => {
(async () => {
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const data = await res.json();
setUsers(data);
} catch (err) {
console.error(err);
}
})();
}, []);
React の useEffect フックで依存配列に要素を持たせない場合の警告を消す方法
React の useEffect フックを使う時に、初回レンダリング時のみに関数を実行したい場合には、第 2 引数である依存配列に要素を持たせない(空配列)ように実装します。
useEffect(() =>{
// 何かしらの関数
} ,[]);
しかし、 TypeScript で useEffect フックを書く場合には、依存する変数を依存配列の要素に持たせないと、 eslint による警告が表示されます。依存配列の設定を忘れないために役立つ警告ですが、 useState フックの setter メソッドを使いたい場合などには、邪魔になります。
React Hook useEffect has missing dependencies: 'var1', 'var2', and 'var3'.
Either include them or remove the dependency array.
(react-hooks/exhaustive-deps) eslint
上記の例のように、 eslint の警告を一時的かつ局所的に消したい場合には、依存配列の直前に「 // eslint-disable-next-line react-hooks/exhaustive-deps 」を書く方法が有効です。この一文を書くと、直後のコード一行( useEffect フックの依存配列)に対して eslint が機能しなくなります。
useEffect(
() => {
// 何かしらの関数
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);