Chapter 09

CRUD処理

kei3dev
kei3dev
2024.07.31に更新

この章からTODOアプリのCRUD処理(Create, Read, Update, Delete)を行っていきます。
画面も作成していきます。
この章ですべて完成となります。

ライブラリのインストール

まず、必要なライブラリをインストールします。

ターミナル
bun add expo-checkbox react-native-root-toast

expo-checkbox

todoのdoneのチェックボックスで使用します。
https://docs.expo.dev/versions/latest/sdk/checkbox/

react-native-root-toast

todoの登録成功時や失敗時のトースト表示で使用します。
https://github.com/magicismight/react-native-root-toast

react-native-root-toastの設定

_layout.tsxRootSiblingParentでラップします。

_layout.tsx
import { useEffect } from "react"
import { SplashScreen, Stack } from "expo-router"
import {
  useFonts,
  NotoSansJP_400Regular,
  NotoSansJP_700Bold,
} from "@expo-google-fonts/noto-sans-jp"
+ import { RootSiblingParent } from "react-native-root-siblings"

SplashScreen.preventAutoHideAsync()

export const RootLayout = () => {
  const [fontsLoaded, fontError] = useFonts({
    NotoSansJP_400Regular,
    NotoSansJP_700Bold,
  })

  useEffect(() => {
    if (fontsLoaded || fontError) {
      SplashScreen.hideAsync()
    }
  }, [fontsLoaded, fontError])

  if (!fontsLoaded && !fontError) {
    return null
  }

  return (
+   <RootSiblingParent>
      <Stack>
        <Stack.Screen name="index" options={{ headerShown: false }} />
      </Stack>
+   </RootSiblingParent>
  )
}

export default RootLayout

todoの型定義

前章でSupabaseのデータベースから生成した型定義を元に、todoの型定義をします。
srcディレクトリ配下にtypesディレクトリを作成し、下記のindex.tsを作成します。

index.ts
import { Database } from "@/supabase/schema"

export type TodoType = Database["public"]["Tables"]["todos"]["Row"]

todoの一覧取得 - Read

トップ画面の index.tsx で todo の一覧を取得します。

index.tsx
import { useEffect, useState } from "react"
import { SafeAreaView } from "react-native-safe-area-context"
import { supabase } from "@/supabase/client"
import { TodoInput } from "../components/TodoInput"
import { TodoList } from "../components/TodoList"
import type { TodoType } from "../types"

const Home = () => {
  const [todos, setTodos] = useState<TodoType[] | null>(null)

  const fetchTodos = async () => {
    const { data } = await supabase.from("todos").select("*")
    setTodos(data)
  }

  useEffect(() => {
    fetchTodos()
  }, [])

  return (
    <SafeAreaView className="flex-1 mx-6 mt-4">
      <TodoInput setTodos={setTodos} />
      <TodoList todos={todos} setTodos={setTodos} />
    </SafeAreaView>
  )
}

export default Home

fetchTodos を 定義し、useEffectで実行します。
取得したデータはstateで保持しておきます。

子コンポーネントの TodoInputsetTodos を渡します。
子コンポーネントの TodoListtodossetTodos を渡します。

todoの作成 - Create

componentsディレクトリ配下に下記のTodoInput.tsxを作成します。
todoの新規作成の入力フィールドとtodoの作成処理を行っています。

TodoInput.tsx
import { useState } from "react"
import type { Dispatch, SetStateAction } from "react"
import { TextInput, View } from "react-native"
import Toast from "react-native-root-toast"
import { supabase } from "@/supabase/client"
import type { TodoType } from "../types"

type Props = {
  setTodos: Dispatch<SetStateAction<TodoType[] | null>>
}

export const TodoInput = ({ setTodos }: Props) => {
  const [task, setTask] = useState("")

  const handleAddTask = async () => {
    if (task.trim().length > 0) {
      const { data, error } = await supabase
        .from("todos")
        .insert([{ task, done: false }])
        .select()

      if (error) {
        Toast.show("登録に失敗しました。")
        return
      }

      if (data && data.length > 0) {
        Toast.show("登録しました!")
        setTodos((prevTodos) => [...(prevTodos || []), data[0]])
        setTask("")
      }
    }
  }

  return (
    <View>
      <TextInput
        className="border border-gray-300 rounded-md p-3 text-xl"
        value={task}
        placeholder="タスクを追加"
        onChangeText={setTask}
        returnKeyType="done"
        onSubmitEditing={handleAddTask}
      />
    </View>
  )
}

handleAddTask

入力されたフィールドが空でないか確認しSupabaseへinsert処理を行います。
成功時と失敗時にトーストを表示します。
insert後は画面に反映するためにsetTodosでデータを反映させます。
フィールドを空にするためにsetTaskで空文字にしておきます。

onChangeText

この属性は、テキスト入力フィールドの内容が変更されるたびに呼び出される関数を指定します。ここでは setTaskが指定されており、入力するたびに、その新しい値でtask状態が更新されます。

returnKeyType="done"

この属性は、モバイルデバイスのソフトキーボード上の「改行」キーの表示を変更します。"done" を指定することで、このキーが「完了」や「確定」を意味するラベルに変更されます。これはユーザーに対して、入力が完了したらこのキーを押すように視覚的に示唆します。

onSubmitEditing

この属性は、ユーザーが入力を終了し、ソフトキーボード上の「完了」キー(上記の returnKeyType="done" で設定したもの)を押した時に実行される関数を指定します。ここではhandleAddTask関数が指定されており、ユーザーが入力を完了して「完了」キーを押すと、新しいタスクが追加されるようになっています。これにより、ユーザーはキーボードから直接タスクを追加できます。

todoリストの表示

componentsディレクトリ配下に下記のTodoList.tsxを作成します。

TodoList.tsx
import type { Dispatch, SetStateAction } from "react"
import { View, FlatList } from "react-native"
import { TodoItem } from "./TodoItem"
import type { TodoType } from "../types"

type Props = {
  todos: TodoType[] | null
  setTodos: Dispatch<SetStateAction<TodoType[] | null>>
}

export const TodoList = ({ todos, setTodos }: Props) => {
  return (
    <View className="my-9">
      <FlatList
        data={todos}
        renderItem={({ item }) => <TodoItem item={item} setTodos={setTodos} />}
        keyExtractor={(item) => item.id.toString()}
        className="border-t"
      />
    </View>
  )
}

個別のtodoの表示 - Update, Delete

componentsディレクトリ配下に下記のTodoItem.tsxを作成します。
このコンポーネント内で todoのdoneの変更todoの編集todoの削除のロジックを定義しています。

TodoItem.tsx
import { useState } from "react"
import type { Dispatch, SetStateAction } from "react"
import { View, Text, TouchableOpacity, TextInput } from "react-native"
import Checkbox from "expo-checkbox"
import { Entypo } from "@expo/vector-icons"
import Toast from "react-native-root-toast"
import { supabase } from "@/supabase/client"
import type { TodoType } from "../types"

type Props = {
  item: TodoType
  setTodos: Dispatch<SetStateAction<TodoType[] | null>>
}

export const TodoItem = ({ item, setTodos }: Props) => {
  const [isEditing, setIsEditing] = useState(false)
  const [editedTask, setEditedTask] = useState(item.task)

  const handleToggleDone = async (isDone: boolean) => {
    try {
      const { data, error } = await supabase
        .from("todos")
        .update({ done: isDone })
        .eq("id", item.id)
        .select()

      if (error) {
        Toast.show("更新に失敗しました。")
        return
      }

      if (data && data.length > 0) {
        setTodos((prevTodos) => {
          if (!prevTodos) return null
          return prevTodos.map((todo) =>
            todo.id === data[0].id ? data[0] : todo
          )
        })
        Toast.show("更新しました!")
      }
    } catch (error) {
      Toast.show("更新に失敗しました。")
    }
  }

  const handleEdit = async () => {
    if (!isEditing) {
      setIsEditing(true)
      return
    }

    try {
      const { data, error } = await supabase
        .from("todos")
        .update({ task: editedTask })
        .eq("id", item.id)
        .select()

      if (error) {
        Toast.show("更新に失敗しました。")
        return
      }

      if (data && data.length > 0) {
        setTodos((prevTodos) => {
          if (!prevTodos) return null
          return prevTodos.map((todo) =>
            todo.id === data[0].id ? data[0] : todo
          )
        })
        Toast.show("更新しました!")
        setIsEditing(false)
      }
    } catch (error) {
      Toast.show("更新に失敗しました。")
    }
  }

  const handleDelete = async () => {
    try {
      const { data, error } = await supabase
        .from("todos")
        .delete()
        .eq("id", item.id)
        .select()

      if (error) {
        Toast.show("削除に失敗しました。")
        return
      }

      if (data && data.length > 0) {
        setTodos((prevTodos) => {
          if (!prevTodos) return null
          return prevTodos.filter((todo) => todo.id !== item.id)
        })
        Toast.show("削除しました!")
      }
    } catch (error) {
      Toast.show("削除に失敗しました。")
    }
  }

  return (
    <View className="flex flex-row items-center justify-between py-2 border-b">
      <View className="flex flex-row items-center gap-3">
        <Checkbox
          value={item.done as boolean}
          onValueChange={(isDone) => handleToggleDone(isDone)}
        />
        {isEditing ? (
          <TextInput
            value={editedTask as string}
            onChangeText={setEditedTask}
            autoFocus
            onSubmitEditing={handleEdit}
            returnKeyType="done"
            className="text-2xl"
          />
        ) : (
          <TouchableOpacity onPress={() => setIsEditing(true)}>
            <Text className={`${item.done ? "line-through" : ""} text-2xl`}>
              {item.task}
            </Text>
          </TouchableOpacity>
        )}
      </View>
      <TouchableOpacity onPress={handleDelete}>
        <Entypo name="circle-with-cross" size={28} color="red" />
      </TouchableOpacity>
    </View>
  )
}
  • handleToggleDonedoneの更新を行います。
  • handleEditでtodoの更新を行います。
  • handleDeleteでtodoの削除を行います。
  • @expo/vector-iconsEntypoで削除アイコンを表示しています。

TouchableOpacity

タッチイベントに反応させるために使います。主にボタンやその他のインタラクティブな要素を作成するために使用されます。

動作確認

これで完成になります。
リファクタリングの余地があるかと思いますが、動作確認をしてみて、CRUD処理が行えるか確認をしてみてください。