たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

React NativeでReact Queryを使ってみよう

React Native React Queryを使ってみようという話。ネタバレすると、今回はuseQueryのみで、useMutationなどは使わない。

TL;DR

今回のコードはこちらのリポジトリで全容を確認できる。

github.com

プロジェクトの準備

まずはプロジェクトを作成する。

expo init --template expo-template-blank-typescript
cd project-name

デザインをしやすくするためにNative Baseをインストールする。

yarn add native-base
expo install react-native-svg

ノッチやステータスバーなOSのインターフェース要素の周辺にコンポーネントを適切に配置するためにreact-native-safe-area-contextをインストールする。このパッケージはnative-baseが依存しているので必要。

expo install react-native-safe-area-context

React Queryをインストールする。

yarn add @tanstack/react-query

React Queryを使ってみる前に必要な作業を行なっていく。

App.tsx

まずは、App.tsxの中身を分離させてMain.tsxとHomeScreen.tsxを作成する。

App.tsxではReact Queryの<QueryClientProvider />やNative Baseの<NativeBaseProvider>などでMain.tsxMainコンポーネントを囲むようにする。こうすることで、Mainコンポーネント内で囲んだプロバイダーのフックが使えるようになる利点がある。

// App.tsx

import { SafeAreaProvider } from "react-native-safe-area-context";
import {
  QueryClient,
  QueryClientProvider,
} from "@tanstack/react-query";
import Main from "./Main";

const queryClient = new QueryClient();

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <SafeAreaProvider>
        <Main />
      </SafeAreaProvider>
    </QueryClientProvider>
  );
}
QueryClient

queryClientはキャッシュされたデータとのコミニュケーションを行うために使用される。そして、基本的にはQueryClientProviderclientプロパティに初期化したqueryClientを一度だけ渡す。

Main.tsx

今回は使っていないが、Main.tsxでは本来React Navigationのルートナビゲーターなどを配置するために使用する。今回はそのままHomeScreenコンポーネントを配置する。

// Main.tsx

import { StatusBar } from "expo-status-bar";
import { HomeScreen } from "./screens/HomeScreen";

export default function Main() {
  return (
    <>
      <HomeScreen />
      <StatusBar style="auto" />
    </>
  );
}

HomeScreen.tsx

では実際にReact Queryを使用するHomeScreenコンポーネントを作成していく。

import { useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import {
  Text,
  Box,
  FlatList,
  VStack,
  HStack,
  Image,
  Spacer,
  Heading,
} from "native-base";

const loader = () => {
  return {
    "2022-07-15": [
      {
        id: "activity-id-1",
        label: "腹筋ローラー",
        date: "2022/7/15",
        value: "20",
        method: "COUNT",
        exp: 100,
        createdAt: "14:25",
        icon: "https://wallpaperaccess.com/full/317501.jpg",
      },
      {
        id: "activity-id-2",
        label: "腹筋ローラー",
        date: "2022/7/15",
        value: "20",
        method: "COUNT",
        exp: 100,
        createdAt: "14:28",
        icon: "https://wallpaperaccess.com/full/317501.jpg",
      },
      {
        id: "activity-id-3",
        label: "腹筋ローラー",
        date: "2022/7/15",
        value: "01:00",
        method: "TIME",
        exp: 200,
        createdAt: "14:31",
        icon: "https://wallpaperaccess.com/full/317501.jpg",
      },
    ],
  };
};

export function HomeScreen() {
  const query = useQuery(["home_screen_loader"], loader);

  return (
    <Box flex={1} safeArea>
      <Box px={3} py={3}>
        <Heading>最近のアクティビティ</Heading>
      </Box>
      <FlatList
        contentContainerStyle={{
          paddingHorizontal: 12,
          paddingVertical: 12,
        }}
        h="1/2"
        showsVerticalScrollIndicator={false}
        showsHorizontalScrollIndicator={false}
        backgroundColor={"dark.800"}
        data={query.data?.["2022-07-15"] ?? []}
        renderItem={({ item }) => {
          const { icon, date, label, value, createdAt, method, exp } = item;
          return (
            <>
              <VStack w="full" borderRadius={10} bg={"white"} px={3} py={3}>
                <HStack mb={3}>
                  <Image
                    source={{
                      uri: icon,
                    }}
                    alt="Alternate Text"
                    size="sm"
                    borderRadius={10}
                  />
                  <VStack px={3}>
                    <Text fontWeight={date === "今日" ? "bold" : "normal"}>
                      {date}
                    </Text>
                    <Text color="dark.400">{label}</Text>
                  </VStack>
                </HStack>
                <HStack>
                  <VStack>
                    <Text fontWeight={"bold"} fontSize={"lg"}>
                      {value}
                    </Text>
                    <Text color="dark.500">
                      {method === "COUNT" ? "回" : "時間"}
                    </Text>
                  </VStack>
                  <Spacer />
                  <VStack>
                    <Text fontWeight={"bold"} fontSize={"lg"}>
                      {exp}
                    </Text>
                    <Text color="dark.500">経験値</Text>
                  </VStack>
                  <Spacer />
                  <VStack>
                    <Text fontWeight={"bold"} fontSize={"lg"}>
                      {createdAt}
                    </Text>
                    <Text color="dark.500">スケジュール</Text>
                  </VStack>
                </HStack>
              </VStack>
              <Box mb={3} />
            </>
          );
        }}
      />
    </Box>
  );
}
クエリーキー

useQueryを使用するとき、第一引数にはクエリーキーを指定する。クエリーキーは1つの文字列を含む配列のようなものから、多数の文字列を含んでいたりオブジェクトを含む配列でも可能です。クエリーキーとして認められる唯一の条件はシリアライズ可能であり、クエリーのデータに対してユニークであることです。

クエリー関数

React Queryがデータを要求するために使用される関数です。クエリー関数の第一引数にはQueryFunctionContextを受け取ります。

解決したデータを返すかエラーをスローするかして必ずプロミスを返さないといけない。undefinedを返すことは許されないので注意が必要。

実行してみる

アプリを起動するとこんな感じに表示される。

これだけだとReact Queryの恩恵がよくわからないかもしれないが、今回で言うとloader関数の部分でFirebase Functionsの呼び出しやREST APIの呼び出し等を行うことで、表示部分とデータの取得に関わる処理の分離がシンプルに実装できるのでコードの管理がとても楽になる。