この記事では、React Nativeで開発したモバイルアプリでTanstack Queryを使ったデータ取得とキャッシュの取り扱いについての注意点を紹介します。また、実例をもとに、誤った実装による不具合とその改修方法を具体的に解説します。

以下のような読者を想定しています。

  • APIを利用したデータのCRUD処理に関心があるフロントエンド開発者
  • React、React Nativeで画面を実装することができる方
  • JavaScriptの非同期処理について実装できる方
  • Tanstack Queryの基本的な仕組みと使い方を理解している方(非同期でデータ取得するときに使用されるライブラリでキャッシュ管理が行えることを知っている方)

React Nativeに関する前提知識は、Fintanが提供している学習教材の「前提知識」や「アプリの実装前に」で説明しています。必要に応じてご確認ください。また、Tanstack Queryについては「React Queryを用いた開発事例の紹介」という記事で使用方法について解説しています(React QueryはTanstack Queryの旧名称です)。

題材のモバイルアプリについて

以下の条件のもとで開発されたモバイルアプリを本記事の題材にしています。

  • モバイルアプリはReact Nativeで開発している
  • Tanstack Queryで非同期の状態管理を行う
  • React Navigationを使用して画面遷移を行っている
  • DBにアカウント情報を保持しておりAPIでデータの登録・取得を行う

モバイルアプリの画面構成は以下の図のようになっています。

  • メールアドレスでログインして使用します。
  • ホーム画面には登録済みのアカウントが一覧表示されています(各アカウントのカードが横スクロールで並んでいます)。1つのメールアドレスに複数のアカウント情報が紐づきます。
  • アプリ内からアカウントの追加が可能です。
  • アカウント詳細画面で各アカウントの詳細情報を参照することができます。
  • 各アカウントが持つスコアという属性はアプリ外から登録・更新されます。

※アカウント更新に関する仕様は本記事の内容では扱わないため省略しています。

ここまでで前提知識の整理は終わりです。ここからデータ管理に関わる不具合について3つの例とその解決方法について紹介していきます。

①前画面に戻ったときデータ更新されていない不具合

戻るボタンで前の画面に戻った際に古いデータが表示されてしまうという問題が発生しました。これは、スマホアプリの画面スタック構造によるもので、戻る遷移をおこなった時は前画面がデータ更新されずそのまま表示されます(参考:「Fintan | React Navigationの基本」)。

例えば、ホーム画面からアカウント詳細画面に遷移したとき、バックエンドからスコアの値として”B”を取得して表示します。スコアはアプリ外から更新されるため、アカウント詳細画面表示中にバックエンドのDBで値が”B”から”A”に値が更新されました。そのあとにアカウント詳細画面からホーム画面へ戻ると、データ更新されないためスコアの値は更新されず以前取得した”B”という値がそのまま表示されます。

画面要件として、戻った後のホーム画面でも最新のデータを表示したいというものがありました。そのためこの挙動は不具合であり、戻る遷移のときにデータを再取得するように修正する必要があります。

戻る画面遷移時にデータを再取得する

対応案は2案あり、Tanstack QueryのuseQueryClient.refetchQueries を使用する案とuseQueryClient.resetQueries を使用する案になります。

refetchQueries を使用すると既存キャッシュを維持しながら最新データに更新することができます。この場合、読み込み中は古いデータを表示し、データ取得完了後に新しいデータに置き換えます。そのため、ローディング表示はありません。

一方で、resetQueries を使用した場合はキャッシュを一旦削除してデータを再取得します。キャッシュがない状態でデータ取得を行うためデータ取得完了まではローディング表示があります。

今回はローディング表示でユーザにデータが更新されたことを伝えたいためresetQueriesを使用する案を採用しました。

不具合箇所の実装について、戻るボタンをタップしたときにTanstack QueryのuseQueryClient.resetQueries でキャッシュを削除するように修正しました。この修正で、戻る遷移時にデータを最新化できるようになりました。

const BackButton = ({ navigation, route }) => {
    const queryClient = useQueryClient();

    const handleBack = async () => {
        // 戻る画面遷移
        navigation.goBack();
        // キャッシュを削除しデータを再取得
        await queryClient.resetQueries(queryKey);      
    };

    return (
        <Button title="戻る" onPress={handleBack} />
    );
};

気を付けるべきポイントは、戻る遷移をキャッシュ削除より先に行うことです。

キャッシュを削除した後に戻る遷移を行うようにすると、キャッシュ削除直後に一瞬アカウント詳細画面がレンダリングされデータのない(読み込み中)状態の画面が表示され、その後にホーム画面に戻るという挙動になります。この挙動はユーザに違和感を与えてしまいます。

キャッシュの削除前に画面遷移するようにしておくことで、読み込み中状態の遷移前画面が表示されることを防ぎます。ホーム画面遷移直後に読み込み中表示になりますが、画面表示直後に読み込み状態になるのは一般的な挙動でありUX的な違和感はないため問題ないと考えています。

②登録したデータが画面に表示されない不具合

ユーザが新しいデータを登録し、ホーム画面に遷移した際に、新規登録したデータが表示されないという問題が発生しました。これは、データ登録後に再取得処理が行われず、コンテキストで保持しているデータが更新されなかったためです。

データ管理ではAPIによるデータの取得だけでなく、それをコンテキストで保持している場合は、その更新も行う必要があります。

データ登録後にコンテキストを更新する

登録完了画面からホーム画面に遷移するときにqueryClient.fetchQueryを使用してデータを再取得し、コンテキストで保持しているデータを更新します。これにより、ホーム画面が表示される際に最新のデータが表示されるようになりました。

const NextButton = ({ navigation }) => {
    const queryClient = useQueryClient();

    // アカウント情報のコンテキストを更新する関数を取得する
    const {setAccounts} = useAccountContext();

    const handleGoHome = async () => {
        try {
            // データの再取得
            const res = await queryClient.fetchQuery(queryKey, queryOptions);

            if (res.data) {
                // 再取得したデータでコンテキストを更新する
                setAccounts(new AppAccounts(res.data));
                // ホーム画面に遷移する
                navigation.navigate('Home');
            }
        } catch (e) {
            // エラー処理
        }
    };

    return (
        <Button title="ホーム画面に進む" onPress={handleGoHome} />
    );
};

③別のメールアドレスでログインすると前回ログインしたユーザの情報が表示される不具合

別のメールアドレスでログインした場合は、そのメールアドレスに紐づく情報が表示されるべきですが、同じ端末でログインした情報が表示されてしまうという問題が発生しました。これは、ログアウト時にキャッシュが削除されていないことが原因です。

ログアウト時にログイン情報に紐づくキャッシュを削除する

ログアウトボタンをタップしたときにメールアドレスに紐づく情報を保持したキャッシュを削除します。コンテキストに同様の情報を保持している場合は、コンテキストのデータも削除しておく必要があります。

ボタンタップ時に実行される関数内で、Tanstack QueryのqueryClient.clear で全てのキャッシュを削除し、コンテキストも初期化するように修正しました。

const LogoutButton = ({ navigation }) => {
    const queryClient = useQueryClient();

    // アカウント情報のコンテキストを更新する関数を取得する
    const {setAccounts} = useAccountContext();

    const handleLogout = useCallback(async () => {
        setLogoutConfirmVisible(false);
        setIsLogout(true);
        try {
            // ログアウト処理
            await logout();
            // キャッシュを削除する
            queryClient.clear();
            // コンテキストを初期化する
            setAccounts(undefined);
        } catch (error) {
            // エラー処理
        }
    }, [logout, queryClient, setAppAccounts]);
    return (
        <Button title="ログアウトする" onPress={handleLogout} />
    );
};

まとめ

この記事では、React Nativeアプリでのデータ管理に関連する3つの不具合とその解決策を説明しました。Tanstack Queryを使用することで、効率的にデータの取得とキャッシュ管理が可能ですが、不正な画面表示を発生させないために適切なタイミングでキャッシュを更新・削除することが重要です。本記事の内容が、皆さんのプロジェクトに役立つことを願っています。

参考文献・URL