ReactとTypeScriptでuseEffectフックの正しい使用方法
React.jsでは副作用を処理するためのフックが2つございます。
・ useEffect
・ useLayoutEffect
これらの主な違いは、詳しくは解説致しませんが、全ては実行時のタイミングです。
簡単に言ってしまえば、非同期なのか同期処理なのかです。
以下で、解説しておりますので参照下さい。
useEffectとは
useEffectフックは2つの引数を受け入れます。
useEffect(() => { // Mounting return () => { // Cleanup function unmount } }, [//Updating]) // 第2引数
デフォルトでは、すべての再レンダリングで実行しますが、オプションとして2番目の引数を渡す事が可能です。
もし、特定の値が変更されたときのみ発生させたい場合は、第2引数でその値を依存関係配列として指定します。
配列が空の場合、useEffectは初回レンダリングでのみ呼び出されます。
また、エフェクトからクリーンアップ関数を返すこともでき、クリーンアップ関数はメモリ領域を防ぐためにコンポーネントがUIから削除される前に実行されます。
any
型の値の配列の場合、そのパラメータはオプションです。
指定しなかった場合は、コンポーネントが更新されるたびに指定した関数が呼び出されます。
もし指定した場合には、Reactはそれらの値が変更されたかどうかをチェックし、違いがあった場合のみ関数を呼び出します。
const [name, setName] = useState('Taro'); useEffect(() => { document.title = `Hello ${name}`; }, [name])
useEffectとuseLayoutEffectフックはどちらも副作用の処理に使われ、オプションでクリーンアップ関数を返すので、値を返さないのであれば型付けは必要ありません。
ですので、これらは非常に簡単です。
import React, {useEffect, useState} from 'react'; const App = () => { const [count, setCount] = useState<number>(0); useEffect(() => { document.title = `The value is ${count}`; }, [count]); return( <div> {count} <button onClick={() => setCount(count + 1)}> + </button> <button onClick={() => setCount(count - 1)}> - </button> </div> ); };
See the Pen React with Typescript useEffectフック by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
暗黙のリターン
しかし、唯一注意しなければいけないのが暗黙のリターンとなります。
つまり、内部のコールバックはuseEffectが何も返さないか、副作用をクリーンアップする関数を返すことが期待されているということです。
場合によっては、TypeScriptを満足させないものを暗黙的に返すことがあります。
したがって、そのような場合これを避ける必要があります。
例えば下記のような、アロー関数をコールバックとして使用する暗黙の場合です。
const Greetings = () => { return 'hey there'; }; useEffect(() => Greetings(), []);
上記での、Greetings
は文字列を返すので、useEffect内のコールバックの戻り値の型も文字列ということになります。
しかしTypeScriptはそれを望んでいません。
アロー関数をコールバックとして使用する場合は暗黙的に値を返さないように注意する必要があります。
基本的にTypeScriptは、関数ではないものやエフェクト関数内でundefined
のものを返そうとすると文句を言い、エラーをスローします。
useEffect(() => { Greetings(); return null; // Error! Type 'null' is not assignable to void | (() => void) } );
他の例を下記に示します。
function DelayedEffect(props: { timerMs: number }) { const { timerMs } = props; useEffect(() => setTimeout(() => { // … }, timerMs), [timerMs] ); //setTimeoutは暗黙のうちに数値を返す //アロー関数本体が中括弧{}で囲まれていません return null; }
これは、useLayoutEffectでも同様で、エフェクトを実行するタイミングが異なるだけです。
useEffectを使うときは、関数かundefined
以外を返さないように注意してください。
そうしないと、TypeScriptとReactの両方から怒られます。
つまり、暗黙的に値を返さないように注意が必要です。関数の本体が中括弧{ }
で囲まれていない場合、暗黙のリターンが発生し、予期しない戻り値が返される可能性があります。
アロー関数を使用する場合は微妙かもしれません。
これらの指摘に従いながら、正しく型安全なReactコンポーネントを作成できるようになります。
useEffectでのasync/await
TypeScriptを使用するかどうかに関わらず、useEffectフックでasync/await関数を使用する際の正しい方法を学んで下さい。
下記は機能はしますが、避ける必要があります。
// ✖︎ これはやめて useEffect(async () => { const json = await fetchUsers("https://jsonplaceholder.typicode.com/users"); setData(json); return () => { // ここを呼ばないとメモリーリークしてしまう }; }, []);
これを見た他のReact開発者は、悲鳴が上がるはずです。
ReactのuseEffectフックは、コンポーネントがアンマウントされたときに呼び出されるクリーンアップ関数が返されることを想定しているためです。
つまり、async関数を使用すると、コールバック関数はクリーンアップ関数ではなくPromiseを返します。
そしてそれが、コンパイラがTypescriptで譲歩している理由となります。
ReactはPromiseを待機していないため、このパターンはTSおよび純粋なVanilla.JSでも機能しません。
上記のようにasync
関数を使うと、クリーンアップ関数が呼び出されないのでバグが発生します。
何も返さないか、関数を返す必要があります。
では、どうすればいいのか?、必ずasync
関数はuseEffectフック内部で使用してください。
基本的には、即座に呼び出される関数式と、名前を付けて呼び出す関数の2つのパターンがございます。
それらを比較してみて、どちらがいいかを判断してください。
// 即時呼び出し // ◯ OK useEffect(() => { (async () => { const json = await fetchUsers("https://jsonplaceholder.typicode.com/users"); setData(json); })(); return () => { //コンポーネントがアンマウントされたときに呼び出されるようになります。 }; }, []);
または、名前付き関数としてuseEffect内部で使用します。
// ◯ OK useEffect(() => { const getUsers = async () => { const json = await fetchUsers("https://jsonplaceholder.typicode.com/users"); setData(data); }; getUsers(); return () => { // コンポーネントがアンマウントされたときに呼び出されるようになります。 }; }, []);
実際に最も厄介なのは、内部関数に重複しない名前を付けなければならないことです。
その代わりとして即時呼び出し関数式を使用したりも可能です。
いずれにせよ、useEffectフックの内部でasync関数を安全に使用できるようになります。
これらによって、クリーンアップ関数を返したいときに、その関数が呼び出されるようになり、useEffectを綺麗に保ち、競合状態から解放されます。
完全に動作する、TypeScriptでの単純なasync/await
構造体でREST APIをfetch
する処理を下記に示します。
カスタムフックを使用しております。
async関数を使用してPromiseを作成し、データを取得します。
URLを含む文字列をパラメーターとして受け取り、ApiResponse
を返す関数としてフックを宣言します。
何か問題が発生した場合は、try/catch
を使用してエラーをキャッチします。
try/catch
ブロック内にawait呼び出しをラップすることは、常にベストプラクティスです。
また、isLoading
をtrue
に設定したため、プロセスは実行中として設定されています。
そして、メインコンポーネントでカスタムフックを呼び出します。
See the Pen React with Typescript fetch useEffect by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記のように場合によっては、データ取得関数をuseEffectの外側に置きたいこともあります。
そのような場合は、なるべく下記のように関数をuseCallbackフックでラップするようにして下さい。
const getApiData = useCallback(async () => { setisLoading(true); try { const apiRes = await fetch(url); const json = await apiRes.json(); setStatus(apiRes.status); setData(json); setUsers(json) } catch (error) { setisError(error); } setisLoading(false); }, []); useEffect(() => { getApiData(); }, [getApiData]); //依存配列に入れる
See the Pen React with Typescript fetch useEffect およびuseCallback by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
なぜわざわざラップするのか?
関数はuseEffectの外部で宣言されているので、フックの依存配列に入れる必要があります。
しかし、もし関数がuseCallbackフックでラップされていなければ、再レンダリングのたびに更新されてしまい、再レンダリングのたびにuseEffectが呼び出されることになります。
それは、あなたが望むことではないはずです。
また、useEffectフックと違い、useCallbackのコールバック関数内で非同期処理を行う場合、関数自体にasyncを付ける必要があります。
つまり、以下は間違いです。
// ✖ const getApiData = useCallback(() => { const fetchData = async () => { // ... }; // ... }, []);
上記では、getApiData関数自体が非同期関数ではないため、正しくない構文となります。
したがって、最初のコードが正しい形式です。getApiData関数自体を非同期関数として宣言し、その中で非同期処理を行います。
最後に
例を見てきて分かる通りTypeScriptでは、useEffectおよびuseLayoutEffectフックに追加の型付けは必要ありませんが、暗黙のリターンおよびフックルールには気をつけるようにして下さい。
asyncリクエストの場合は、react-queryパッケージを使用することをお勧めします。
ReactおよびNext.jsアプリでのサーバー状態の取得、キャッシュ、同期、および更新を簡単にする React用のデータ取得ライブラリとなります。
これはリクエストごとにisLoading
とisError
を返しまた、ローカルに状態を追加する必要はありません。
つまり、グローバルな状態を変更することなく、シンプルかつ宣言的な方法でReactベースのアプリケーションのデータをフェッチ、キャッシュ、および更新できます。
react-queryを使うと、すべてのコードを4行に減らすことができます。
本日は以上となります。
最後までこの記事を読んで頂きありがとうございます。