DEV Community

sandatya widhi
sandatya widhi

Posted on

12. Membuat Fitur CRUD (Studi Kasus Aplikasi To-Do-List

kita lanjutkan project sebelumnya dimana pada tutorial sebelumnya kita sudah berhasil membuat login, signup dan authentikasi ke firestore database. Pada tutorial ini kita akan lanjutkan dengan menambahkan fitur CRUD.

A. Buat struktur komponen

buat folder baru dengan nama components pada folder (screens) yang berisi file EditTodoModal.jsx, Todoinput.jsx, Todoitem.jsx dan TodoList.jsx .

berikut struktur lengkapnya :

Image description

B. 📄 components/Todoinput.jsx

import React from "react";
import { TextInput, Button, View } from "react-native";

export default function TodoInput({ todo, setTodo, addTodo }) {
  return (
    <View>
      <TextInput placeholder="Tambah to-do..." value={todo} onChangeText={setTodo} style={{ borderWidth: 1, padding: 10, marginBottom: 10 }} />
      <Button title="Tambah" onPress={addTodo} />
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

Image description

C. 📄 components/Todoitem.jsx

import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";

export default function TodoItem({ item, onToggle, onDelete, onEdit }) {
  return (
    <View style={styles.item}>
      <TouchableOpacity onPress={() => onToggle(item.id, item.completed)} style={styles.textContainer}>
        <Text style={[styles.text, item.completed && styles.completed]}>{item.title}</Text>
      </TouchableOpacity>
      <View style={styles.actions}>
        <TouchableOpacity onPress={() => onEdit(item)}>
          <Text style={styles.actionText}>✏️</Text>
        </TouchableOpacity>
        <TouchableOpacity onPress={() => onDelete(item.id)}>
          <Text style={styles.actionText}>🗑️</Text>
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  item: {
    paddingVertical: 12,
    paddingHorizontal: 16,
    backgroundColor: "#f1f1f1",
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    borderBottomWidth: 1,
    borderBottomColor: "#ccc", // Garis pembatas
  },
  textContainer: {
    flex: 1,
  },
  text: {
    fontSize: 16,
  },
  completed: {
    textDecorationLine: "line-through",
    color: "gray",
  },
  actions: {
    flexDirection: "row",
    marginLeft: 10,
  },
  actionText: {
    fontSize: 18,
    marginLeft: 10,
  },
});
Enter fullscreen mode Exit fullscreen mode

Komponen TodoItem ini berfungsi untuk menampilkan satu item todo (tugas)
Image description

D. 📄 components/TodoList.jsx

import React from "react";
import { FlatList } from "react-native";
import TodoItem from "./Todoitem";

export default function TodoList({ todos, onToggle, onDelete, onEdit }) {
  return <FlatList data={todos} keyExtractor={(item) => item.id} renderItem={({ item }) => <TodoItem item={item} onToggle={onToggle} onDelete={onDelete} onEdit={onEdit} />} />;
}
Enter fullscreen mode Exit fullscreen mode

Komponen TodoList ini digunakan untuk menampilkan daftar to-do secara otomatis dan efisien dalam bentuk list menggunakan FlatList dari React Native.
Image description

E. 📄 components/EditTodoModal.jsx

import React from "react";
import { Modal, View, Text, TextInput, Button } from "react-native";

export default function EditTodoModal({ visible, editText, setEditText, onSave, onCancel }) {
  return (
    <Modal visible={visible} animationType="slide" transparent={true} onRequestClose={onCancel}>
      <View style={{ flex: 1, backgroundColor: "#000000aa", justifyContent: "center", alignItems: "center" }}>
        <View style={{ backgroundColor: "white", padding: 20, width: "80%", borderRadius: 10 }}>
          <Text style={{ fontSize: 18, marginBottom: 10 }}>Edit To-Do</Text>
          <TextInput value={editText} onChangeText={setEditText} style={{ borderWidth: 1, padding: 10, marginBottom: 10 }} />
          <Button title="Simpan" onPress={onSave} />
          <View style={{ marginTop: 10 }} />
          <Button title="Batal" color="gray" onPress={onCancel} />
        </View>
      </View>
    </Modal>
  );
}
Enter fullscreen mode Exit fullscreen mode

Komponen EditTodoModal digunakan untuk menampilkan pop-up (modal) saat pengguna ingin mengedit to-do.

Image description

F. Update di Home.jsx

import React, { useEffect, useState } from "react";
import { View, Text, TextInput, Button, FlatList, Modal, Alert, ActivityIndicator } from "react-native";
import { useRouter } from "expo-router";
import { signOut, onAuthStateChanged } from "firebase/auth";
import { auth, db } from "../../config/firebase";
import { collection, addDoc, query, where, onSnapshot, deleteDoc, updateDoc, doc, orderBy } from "firebase/firestore";
import TodoItem from "./components/Todoitem"; // Pastikan file Todoitem.jsx ada di folder components

export default function Home() {
  const [user, setUser] = useState(null);
  const [loadingUser, setLoadingUser] = useState(true);
  const [todo, setTodo] = useState("");
  const [todos, setTodos] = useState([]);
  const [editModalVisible, setEditModalVisible] = useState(false);
  const [editTodo, setEditTodo] = useState(null);
  const [editText, setEditText] = useState("");
  const router = useRouter();

  // Cek user login
  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      if (!currentUser) {
        router.replace("/Login");
      } else {
        setUser(currentUser);
      }
      setLoadingUser(false);
    });

    return unsubscribe;
  }, []);

  // Ambil data todo dari Firestore
  useEffect(() => {
    if (!user) return;

    const q = query(collection(db, "todos"), where("userId", "==", user.uid), orderBy("createdAt", "desc"));

    const unsubscribe = onSnapshot(q, (snapshot) => {
      const list = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
      setTodos(list);
    });

    return unsubscribe;
  }, [user]);

  const addTodo = async () => {
    if (todo.trim() === "") return;

    await addDoc(collection(db, "todos"), {
      title: todo,
      completed: false,
      createdAt: new Date(),
      userId: user.uid,
    });

    setTodo("");
  };

  const toggleComplete = async (id, status) => {
    await updateDoc(doc(db, "todos", id), { completed: !status });
  };

  const deleteTodo = async (id) => {
    await deleteDoc(doc(db, "todos", id));
  };

  const openEditModal = (item) => {
    setEditTodo(item);
    setEditText(item.title);
    setEditModalVisible(true);
  };

  const handleEditSave = async () => {
    if (!editText.trim()) {
      Alert.alert("Isi tidak boleh kosong");
      return;
    }

    await updateDoc(doc(db, "todos", editTodo.id), {
      title: editText,
    });

    setEditModalVisible(false);
    setEditTodo(null);
    setEditText("");
  };

  const handleLogout = async () => {
    await signOut(auth);

    //tambahkan untuk membersihkan textinput email dan password
    router.replace({ pathname: "/Login", params: { reset: "true" } });
  };

  // Tampilkan loading selama pengecekan user
  if (loadingUser) {
    return (
      <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text style={{ fontSize: 24, fontWeight: "bold", marginBottom: 10 }}>To-Do List</Text>
      <Text style={{ marginBottom: 5 }}>Welcome, {user?.email}</Text>
      <TextInput
        placeholder="Tambah to-do..."
        value={todo}
        onChangeText={setTodo}
        style={{
          borderWidth: 1,
          borderColor: "#ccc",
          padding: 10,
          marginBottom: 10,
          borderRadius: 5,
        }}
      />

      <Button title="Tambah" onPress={addTodo} />

      <FlatList
        data={todos}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => <TodoItem item={item} onToggle={toggleComplete} onDelete={deleteTodo} onEdit={openEditModal} />}
        ListEmptyComponent={<Text style={{ marginTop: 20 }}>Belum ada todo</Text>}
      />

      <View style={{ marginTop: 20 }}>
        <Button title="Logout" color="red" onPress={handleLogout} />
      </View>

      {/* Modal Edit */}
      <Modal visible={editModalVisible} animationType="slide" transparent={true} onRequestClose={() => setEditModalVisible(false)}>
        <View
          style={{
            flex: 1,
            backgroundColor: "#000000aa",
            justifyContent: "center",
            alignItems: "center",
          }}
        >
          <View
            style={{
              backgroundColor: "white",
              padding: 20,
              width: "80%",
              borderRadius: 10,
            }}
          >
            <Text style={{ fontSize: 18, marginBottom: 10 }}>Edit To-Do</Text>
            <TextInput value={editText} onChangeText={setEditText} style={{ borderWidth: 1, padding: 10, marginBottom: 10 }} />
            <Button title="Simpan" onPress={handleEditSave} />
            <View style={{ marginTop: 10 }} />
            <Button title="Batal" color="gray" onPress={() => setEditModalVisible(false)} />
          </View>
        </View>
      </Modal>
    </View>
  );
}

Enter fullscreen mode Exit fullscreen mode

Komponen ini adalah halaman utama aplikasi To-Do List. Di sini pengguna bisa:

  • Melihat daftar to-do miliknya
  • Menambahkan to-do baru
  • Mengedit to-do
  • Menandai to-do sebagai selesai
  • Menghapus to-do
  • Logout dari akun

Image description

Image description

Image description

Berikut adalah tampilan aplikasi :

Image description

Image description

Image description

ERROR YANG UMUMNYA TERJADI

tambah data sudah bisa namun ketika browser di refresh data tersebut hilang pada aplikasi , dan data juga tidak bisa di edit serta di hapus secara real time .
ada pesan error seperti ini pada browser:

C:\Users\User\Documents\reactnative\ReactFirebaseApp\node_modules\@expo\metro-runtime\src\error-overlay\LogBox.web.ts:134  [2025-04-08T07:31:21.400Z]  @firebase/firestore: Firestore (11.4.0): Uncaught Error in snapshot listener: FirebaseError: [code=failed-precondition]: The query requires an index. You can create it here: https://console.firebase.google.com/v1/r/project/react-native-project-1-cd8e8/firestore/indexes?create_composite=Clpwcm9qZWN0cy9yZWFjdC1uYXRpdmUtcHJvamVjdC0xLWNkOGU4L2RhdGFiYXNlcy8oZGVmYXVsdCkvY29sbGVjdGlvbkdyb3Vwcy90b2Rvcy9pbmRleGVzL18QARoKCgZ1c2VySWQQARoNCgljcmVhdGVkQXQQAhoMCghfX25hbWVfXxAC


Enter fullscreen mode Exit fullscreen mode

SOLUSI :
error yang dialami muncul karena Firestore memerlukan composite index untuk query yang kamu buat dengan kombinasi where("userId", "==", ...) dan orderBy("createdAt", "desc").
🔧 Cara Mengatasinya
Klik link di error:
sebagai contoh link : https://console.firebase.google.com/v1/r/project/react-native-project-1-cd8e8/firestore/indexes?create_composite=Clpwcm9qZWN0cy9yZWFjdC1uYXRpdmUtcHJvamVjdC0xLWNkOGU4L2RhdGFiYXNlcy8oZGVmYXVsdCkvY29sbGVjdGlvbkdyb3Vwcy90b2Rvcy9pbmRleGVzL18QARoKCgZ1c2VySWQQARoNCgljcmVhdGVkQXQQAhoMCghfX25hbWVfXxAC

Setelah terbuka, langsung klik “Create Index” / “Buat Indeks”. Tunggu beberapa saat hingga index selesai dibuat (1–2 menit biasanya).

Mengapa Data Hilang Saat Refresh?
Sebenarnya datanya tidak hilang, hanya saja query-nya gagal dieksekusi karena belum ada index, jadi onSnapshot() tidak berhasil mengambil data.

Tentang Edit & Hapus yang Tidak Bekerja Real-Time
Setelah index berhasil dibuat, semua fitur seperti:

  • Real-time onSnapshot()
  • Edit dengan updateDoc()
  • Delete dengan deleteDoc() …akan berfungsi seperti yang diharapkan.

Top comments (0)