やりたいこと

  • React Nativeのアプリで
  • 画面遷移はReact Navigationを利用しており
  • ノートアプリのように、一覧画面と詳細画面があるようなアプリで
  • 詳細画面(子画面)で行った変更に合わせて一覧画面(親画面)の情報(state)を書き換えたい

これがReact Navigationを使っていると意外と難しい

React Navigationを使っていると子画面から親画面を更新することが非常に難しい。 真っ先に思いつくのは、親画面から子画面に対して親画面を更新するメソッドをroute.paramsで渡すことだが、React Navigationではそれを禁止している。 これができると万事解決なのだが、他の方法を考える必要がある。

公式では子画面から親画面にparamsを渡す方法として、navigateメソッドを使って戻る遷移をするときにparamsを渡す方法、親画面を更新するメソッドを子画面で実行する方法としてContext使う方法などが紹介されている。 しかし、戻る遷移の時にデータを渡す方法では戻ったタイミングで画面が再レンダリングされるのでやや見た目がよくないし子画面で親画面に渡すためのfetchメソッドを実行するのに違和感がある。Contextも、親画面を更新するためだけは使いたくない。(個人の感想)

あるいはReduxやRecoilなどのグローバルなステート管理を導入すれば解決できるが、さくっと作り切りたい個人開発アプリ程度ではそれもちょっと手間である。(個人の感想) 最近の流行り的にもReduxのような重めなライブラリではなくReact QueryやSWRのように、データを使うコンポーネントが自信でフェッチすればいいじゃんという考えが主流になってきている気がする。(個人の感想)

他には、navigation.addListener("focus", フェッチメソッド)のように画面を表示した時にフェッチメソッドを実行する方法なども考えられる。しかしこれも画面表示のタイミングでフェッチするためタイムラグが生まれてしまう。

React Queryのキャッシュ機能を使えば解決!

そこで今回はReact Queryのキャッシュ機能を使ってこの問題を解決しようと思う。 React Queryはキャッシュ機能が優秀で、これのおかげで一度データをフェッチした画面を表示するときはとりあえずキャッシュを使って即時で画面をレンダリングし、あとからフェッチが完了したら最新のデータに置き換えるみたいなことをしてくれる。 また、queryClient.invalidateQueries(QueryKey)をつかい、任意のキャッシュを明示的に無効化することで、任意のタイミングでリフェッチを実行することもできる。

今回はこれを使い、ノートアプリを想定し、子画面で新規ノートを作成したら、親画面のノート一覧がリフェッチされ更新されるというサンプルを作ってみた。 重要なポイントは下記のサンプルの中に書き込んでいるので、それを参照してほしい。

※下記はReact Queryの説明のためのサンプルであるため、typescriptの型付けやstyleなどは適切でない or 省略されていることを了承いただきたい。

React Queryを使った子画面から親画面を更新するサンプルコード

App.tsx

import { StatusBar } from "expo-status-bar";
import { StyleSheet, SafeAreaView } from "react-native";
import { QueryClient, QueryClientProvider } from "react-query";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeScreen from "./src/screens/HomeScreen";
import NoteEditScreen from "./src/screens/NoteEditScreen";

const queryClient = new QueryClient();

const Stack = createNativeStackNavigator();

const App = () => (
  <QueryClientProvider client={queryClient}>
    <SafeAreaView>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen
            name="HomeScreen"
            component={HomeScreen}
            options={{ title: "ノート" }}
          />
          <Stack.Screen
            name="NoteEditScreen"
            component={NoteEditScreen}
            options={{ title: "" }}
          />
        </Stack.Navigator>
      </NavigationContainer>
      <StatusBar style="auto" />
    </SafeAreaView>
  </QueryClientProvider>
);

export default App;

HomeScreen.tsx

import axios from "axios";
import React from "react";
import {
  Text,
  FlatList,
  TouchableOpacity,
  View,
} from "react-native";
import { useQuery } from "react-query";

const HomeScreen = ({ navigation }): JSX.Element => {
  // NoteやRequestErrorは自作の型。別途d.tsファイルで宣言している。
  const { isLoading, error, data } = useQuery<Note[], RequestError>(
    "notes", //←ここで任意のQueryKeyを指定する。React QueryはQueryKeyでキャッシュを管理している。
    async () => {
      const response = await axios.get<Note[]>("notes");
      return response.data;
    },
  );

  if (isLoading) return <Text>Loading...</Text>;
  if (error) {
    return <Text>An error has occurred:{error.code}</Text>;
  }

  const renderItem = ({ item }: { item: Note }) => (
    <View>
      <Text>{item.title}</Text>
      <Text>{item.body}</Text>
    </View>
  );

  return (
    <View >
      <FlatList
        data={data}
        renderItem={renderItem}
        keyExtractor={(item) => item.id!}
      />
      {/* ボタンを押したらノート編集画面に遷移する */}
      <TouchableOpacity onPress={() => navigation.navigate("NoteEditScreen")} />
    </View>
  );
};

export default HomeScreen;

NoteEditScreen.tsx

import React from "react";
import { View, TouchableOpacity } from "react-native";
import { useQueryClient, useMutation } from "react-query";
import axios from "axios";

const NoteEditScreen = (): JSX.Element => {
  // useQueryClientからApp.tsxで宣言したqueryClientを入手する。
  const queryClient = useQueryClient();

  const mutation = useMutation<Note, RequestError, Note>(
    "note",
    async (note) => {
      const response = await axios.post<Note>("http://localhost:3000/notes", note);
      // postが成功したタイミングでHomeScreenのuseQueryで指定したものと同じQueryKeyを指定してキャッシュを無効化する。
      // キャッシュが無効化されると、HomeScreenのuseQueryはリフェッチを行い、一覧画面が更新される。
      queryClient.invalidateQueries("notes");
      return response.data;
    },
  );

  return (
    <View>
      // 今回はボタンを押したら新規ノートを作成するだけの簡単な実装。
      <TouchableOpacity onPress={() => mutation.mutate({title: "こねるの", body: "短縮"})} />
    </View>
  );
};

export default NoteEditScreen;

まとめ

  • React Navigationでは子画面から親画面を更新することは難しい。
  • queryClient.invalidateQueries(QueryKey)を子画面で実行すれば、任意のタイミングで親画面のuseQuetyをリフェッチさせることができる。