この章からTODOアプリのCRUD処理(Create, Read, Update, Delete)を行っていきます。
画面も作成していきます。
この章ですべて完成となります。
ライブラリのインストール
まず、必要なライブラリをインストールします。
bun add expo-checkbox react-native-root-toast
expo-checkbox
todoのdone
のチェックボックスで使用します。
react-native-root-toast
todoの登録成功時や失敗時のトースト表示で使用します。
react-native-root-toast
の設定
_layout.tsx
を RootSiblingParent
でラップします。
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
を作成します。
import { Database } from "@/supabase/schema"
export type TodoType = Database["public"]["Tables"]["todos"]["Row"]
Read
todoの一覧取得 - トップ画面の index.tsx
で todo の一覧を取得します。
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
で保持しておきます。
子コンポーネントの TodoInput
にsetTodos
を渡します。
子コンポーネントの TodoList
に todos
と setTodos
を渡します。
Create
todoの作成 - components
ディレクトリ配下に下記のTodoInput.tsx
を作成します。
todoの新規作成の入力フィールドとtodoの作成処理を行っています。
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
を作成します。
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>
)
}
Update
, Delete
個別のtodoの表示 - components
ディレクトリ配下に下記のTodoItem.tsx
を作成します。
このコンポーネント内で todoのdoneの変更
、todoの編集
、todoの削除
のロジックを定義しています。
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>
)
}
-
handleToggleDone
でdone
の更新を行います。 -
handleEdit
でtodoの更新を行います。 -
handleDelete
でtodoの削除を行います。 -
@expo/vector-icons
のEntypo
で削除アイコンを表示しています。
TouchableOpacity
タッチイベントに反応させるために使います。主にボタンやその他のインタラクティブな要素を作成するために使用されます。
動作確認
これで完成になります。
リファクタリングの余地があるかと思いますが、動作確認をしてみて、CRUD処理が行えるか確認をしてみてください。