Gemcook Tech Blog
🤷‍♂️

useFocusEffectって?

2024/10/15に公開

はじめに

みなさん、こんにちは!今回は、React NativeのReact NavigationでよくつまずきがちなuseEffectとuseFocusEffectの違いについてまとめてみました!両hooksともに似てるようで違うので、使い分けに悩んだ経験がある人も多いんじゃないでしょうか?

useFocusEffectって何?

useFocusEffectはReact Navigationが提供してるカスタムフックです。画面がフォーカスされたときに何かしたい!っていう時に使用します。

import { useFocusEffect } from "@react-navigation/native";
import { useCallback } from "react";
import { View } from "react-native";

function MyScreen() {
  useFocusEffect(
    useCallback(() => {
      console.log("画面がフォーカスされたよ!");

      return () => {
        console.log("フォーカスが外れたよ!");
      };
    }, []),
  );

  return <View>...</View>;
}

画面が表示されたり、ユーザーが画面に戻ってきたりするたびに動きます。便利!!

useEffectとuseFocusEffectの違い

書き方

useEffectとは少し書き方が違います。useFocusEffect内でuseCallbackを使用してます。

コールバック関数をメモ化しています。これにより、不要な再レンダリングを防ぎ、パフォーマンスを向上させることができます!

To avoid the running the effect too often, it's important to wrap the callback in useCallback before passing it to useFocusEffect as shown in the example.

公式も述べていますが、重要なのは、このコールバック関数をuseCallbackでメモ化することです。なので、画面のフォーカス状態が変わるたびに無駄なレンダリングが発生するのを防ぐことができます。

useEffectのuseCallbackの使い方
import { useCallback, useEffect } from "react";

function MyComponent() {
  const handleSomeAction = useCallback(() => {
    // 何らかの処理
    console.log("Action performed");
  }, []); // 依存配列は空

  useEffect(() => {
    // コンポーネントがマウントされたときに実行
    handleSomeAction();

    return () => {
      // クリーンアップ関数(オプション)
      console.log("Component will unmount");
    };
  }, [handleSomeAction]); // handleSomeActionを依存配列に追加

  // ...
}

スコープ

useEffectはReact全般で使えますが、useFocusEffectはReact NativeのReact Navigation or Expo RouterなどReact Native専用のライブラリで使用可能です。つまり、useFocusEffectは「画面」の概念がある時に使います。

実行のタイミング

useEffectは、コンポーネントがマウントされたときや、依存配列の値が変わったときに実行します。

一方、useFocusEffectは画面がフォーカスを受け取るたびに実行します。useEffectの方が、「コンポーネントの生涯」を通じて一貫した動きをするって感じですね。useFocusEffectは、「ユーザーの注目」に合わせて動くことになります。

パフォーマンス

useEffectは、レンダリングのたびにチェックされるので、使い方によってはパフォーマンスに影響が出るかと思います。useFocusEffectは、フォーカスの変更時だけ動くので、パフォーマンスへの影響は小さくなります。

useFocusEffectはどんな時に使うの?

useFocusEffectは、以下の場面などで活躍します!

  • ユーザーが画面に戻ってきたときに、最新のデータを表示したいとき(データの再フェッチ)
  • 特定の画面が表示されたときに、ステータスバーの色を変えたりなど(画面固有の設定)
  • 画面が表示されたときに、ログインしてなかったらログイン画面に飛ばすなど(条件付きナビゲーション)

クリーンアップの使い方

両方のフックでクリーンアップ関数が使えますが、少し違いがあります。useEffectのクリーンアップは、コンポーネントがアンマウントされるときや、次のエフェクトが実行される直前に呼ばれます。

一方、useFocusEffectのクリーンアップは、画面がフォーカスを失うたびに呼ばれます。

useFocusEffect(
  useCallback(() => {
    const interval = setInterval(() => {
      console.log("ティックトック");
    }, 1000);

    return () => {
      clearInterval(interval);
      console.log("さようなら、インターバル!");
    };
  }, []),
);

上の例題のような感じで、画面を離れるたびにインターバルをクリアできます!

クリーンアップで迷ったこと

クリーンアップ関数のところで迷ったところがあったので共有したいです!

useFocusEffectを使う際、クリーンアップ関数の役割について誤解しやすい点がありました。以下では、その重要なポイントを解説します。

useFocusEffectを使うにあたってReact Navigation公式は以下のように述べています。

Like useEffect, a cleanup function can be returned from the effect in useFocusEffect. The cleanup function is intended to cleanup the effect - e.g. abort an asynchronous task, unsubscribe from an event listener, etc. It's not intended to be used to do something on blur.

クリーンアップ関数は、非同期タスクの中止、イベントリスナーの登録解除など、エフェクトをクリーンアップするためのもので、blur上で何かをするために使われることは意図されていない。クリーンアップ関数は、エフェクトのクリーンアップが必要なとき、つまり、blur、unmount、依存関係の変更などのときに実行されます。 この関数は、ステートを更新したり、blur時に起こるべきことをするのには適していません。 代わりにblurイベントをリッスンする必要がある。

クリーンアップ関数の本当の目的

クリーンアップ関数は、エフェクト自体をきれいに片付けるためのものです。

  • 非同期タスクを中断する
  • タイマーをクリアする
  • イベントリスナーを解除する etc...

これらが主な役割です。

よくある誤解

ここで気をつけたいのは、クリーンアップ関数を「画面がblurになったときに何かを実行する場所」と勘違いしてしまうことです。これは正しくありません。

useFocusEffect(
  useCallback(() => {
    // フォーカス時の処理
    return () => {
      // ❌ 間違い:blur時に新しい処理を実行しようとしている
      saveDraftData();
    };
  }, []),
);

この例では、クリーンアップ関数内でsaveDraftData()を呼び出しています。これは、フォーカスが外れたタイミングで特定の処理(ドラフトデータの保存)を行おうとしている誤った使い方です。

要するに、クリーンアップ関数は「エフェクトが設定したものを片付ける」ためのものであり、「画面がblurしたときに新しい処理を行う」ためのものではありません。この区別を理解し、適切に使用することで、より安全で予測可能なコードを書くことができます。

ではblur時(ユーザーが画面から離れたとき)ではどうするのか?

もし、「blur時に何かしたい」という場合は、useFocusEffectではなく、ナビゲーションのイベントリスナーを使う方法がReact Navigation公式では推奨されます。React Navigationはfocusとblurのイベントを提供しているので、これを活用することでより明確に「blur時の処理」を行うことができます。

function MyScreen() {
  const navigation = useNavigation();

  useEffect(() => {
    const unsubscribeBlur = navigation.addListener("blur", () => {
      // 画面がblurされた時の処理
      console.log("画面がblurされたよ!");
    });
    return unsubscribeBlur;
  }, [navigation]);

  return <View>...</View>;
}

https://reactnavigation.org/docs/use-focus-effect/#when-to-use-focus-and-blur-events-instead

useEffectで画面に入ってきたときの処理を書いたらどうなるのか?

基本的に、画面が遷移した時にはuseEffectは実行されます。ただ、モーダルからの遷移の戻りであったり、スワイプジェスチャーの戻りで遷移した場合にはuseEffectの依存配列をしっかりと設定していない場合は画面のフォーカス状態と関係なく実行されません。つまりuseEffectはReact Navigationの画面のフォーカス状態を直接考慮しないため、ナビゲーションに関連する処理が適切に行われないということです。

例えばですがモーダルから戻った時にカウントアップしてほしいときを分けて書いてみました。

useEffectを使った場合

const HomePage = () => {
  const router = useRouter();
  const [count, setCount] = useState(0);
  useEffect(() => {
    setCount((prev) => prev + 1);
  }, []);

  return (
    <View style={{ flex: 1, justifyContent: "center" }}>
      <Button title="Go to Home" onPress={() => router.navigate("/")} />
      <Text style={{ textAlign: "center", fontSize: 35 }}>{count}</Text>
      <Button title="Go to modal" onPress={() => router.navigate("/modal")} />
    </View>
  );
};

useFocusEffectを使った場合

const HomePage = () => {
  const router = useRouter();
  const [count, setCount] = useState(0);
  useFocusEffect(
    useCallback(() => {
      setCount((prev) => prev + 1);
    }, []),
  );

  return (
    <View style={{ flex: 1, justifyContent: "center" }}>
      <Button title="Go to Home" onPress={() => router.navigate("/")} />
      <Text style={{ textAlign: "center", fontSize: 35 }}>{count}</Text>
      <Button title="Go to modal" onPress={() => router.navigate("/modal")} />
    </View>
  );
};

二つを比べてもらってわかる通り、useEffectの本来の期待する挙動ではないですね。つまりカウントアップが実行されていません。このようにある一定の条件の場合ではuseEffectが実行されず期待する結果にならないので、useFocusEffectを使うことで画面にフォーカスが当たった時に実行してカウントアップすることができます。

useIsFocusedについて(おまけ)

このフックを呼び出すと、現在の画面がフォーカスされているかどうかをtrueかfalseで返します。

useEffect内と併用することで使用すれば、useFocusEffectのような使い方が可能になりますね。

const isFocused = useIsFocused();

useEffect(() => {
  if (isFocused) {
    setCount((prev) => prev + 1);
  }
}, [isFocused]);

上記のように使用するなら僕はuseFocusEffectを使用した方がわかりやすくていいのかなとは思いますが。。。

https://reactnavigation.org/docs/use-is-focused/

終わり

結局のところ、useEffectとuseFocusEffectは似てるようで違う用途があります。useEffectはコンポーネントの全般を見る、useFocusEffectは画面の注目度を見るみたいな感じでしょうか。

適材適所で使い分けることで、アプリの動きがスムーズになるかと思います。ぜひ、自分のプロジェクトで試してみてください!どっちを使うべきか迷ったら、「このエフェクト、画面のフォーカスに関係ある?」って自問自答してみるといいかもしれません。

React Nativeの世界は奥が深いですよね。でも、こういった小さな違いを理解していくことで、どんどん良いアプリが作れるようになると思います!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion