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 :
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>
);
}
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,
},
});
Komponen TodoItem ini berfungsi untuk menampilkan satu item todo (tugas)
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} />} />;
}
Komponen TodoList ini digunakan untuk menampilkan daftar to-do secara otomatis dan efisien dalam bentuk list menggunakan FlatList dari React Native.
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>
);
}
Komponen EditTodoModal digunakan untuk menampilkan pop-up (modal) saat pengguna ingin mengedit to-do.
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>
);
}
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
Berikut adalah tampilan aplikasi :
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
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)