ReactとTypeScriptでuseEffectフックの正しい使用方法

React.jsでは副作用を処理するためのフックが2つございます。

useEffect

useLayoutEffect

これらの主な違いは、詳しくは解説致しませんが、全ては実行時のタイミングです。

簡単に言ってしまえば、非同期なのか同期処理なのかです。

以下で、解説しておりますので参照下さい。

dev-k.hatenablog.com

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 APIfetchする処理を下記に示します。

カスタムフックを使用しております。

async関数を使用してPromiseを作成し、データを取得します。

URLを含む文字列をパラメーターとして受け取り、ApiResponseを返す関数としてフックを宣言します。

何か問題が発生した場合は、try/catchを使用してエラーをキャッチします。

try/catchブロック内にawait呼び出しをラップすることは、常にベストプラクティスです。

また、isLoadingtrueに設定したため、プロセスは実行中として設定されています。

そして、メインコンポーネントでカスタムフックを呼び出します。

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用のデータ取得ライブラリとなります。

これはリクエストごとにisLoadingisErrorを返しまた、ローカルに状態を追加する必要はありません。

つまり、グローバルな状態を変更することなく、シンプルかつ宣言的な方法でReactベースのアプリケーションのデータをフェッチ、キャッシュ、および更新できます。

react-queryを使うと、すべてのコードを4行に減らすことができます。

本日は以上となります。

最後までこの記事を読んで頂きありがとうございます。

プライバシーポリシー

© 2023 Icons8 LLC. All rights reserved.

© [deve.K], 2023. React logo is a trademark of Facebook, Inc. JavaScript is a trademark of Oracle Corporation and/or its affiliates. jQuery and the jQuery logo are trademarks of the JS Foundation. TypeScript and the TypeScript logo are trademarks of the Microsoft Corporation. Next.js and the Next.js logo are trademarks of Vercel, Inc. Firebase and the Firebase logo are trademarks of Google LLC. All logos edited by [deve.K].