Studi Kasus To-Do: Komponen List & Item
Lanjutkan proyek To-Do List React+TS! Bangun komponen `TodoList.tsx` untuk merender daftar tugas dan `TodoItem.tsx` untuk menampilkan setiap tugas individual, lengkap dengan props yang diketik.
Proyek To-Do List (React + TS) #3: Nampilin Daftar Tugasnya Biar Kece!
Udah bisa nambahin tugas baru ke state todos
di App.tsx
lewat TodoForm
? Mantap! Sekarang, tugas-tugas yang udah disimpen itu mau kita tampilin dong di halaman biar keliatan.
Kita bakal bikin dua komponen baru buat ini:
TodoItem.tsx
: Komponen kecil yang tugasnya nampilin satu item tugas individual. Dia bakal nerima data satu tugas (ID, teks, status selesai) sebagai props.TodoList.tsx
: Komponen yang tugasnya ngerender keseluruhan daftar tugas. Dia bakal nerima arraytodos
dariApp.tsx
sebagai prop, terus nge-map array itu jadi sekumpulan komponenTodoItem
.
Ini contoh bagus buat liat gimana kita mecah UI jadi komponen yang lebih kecil dan reusable, dan gimana data (props) ngalir dari parent ke child.
Langkah 1: Membuat Komponen TodoItem.tsx
Komponen ini yang paling spesifik, ngurusin tampilan satu biji tugas.
-
Di folder
src/components/
, bikin file baruTodoItem.tsx
. -
Isi kodenya:
File
src/components/TodoItem.tsx
:tsx
import React from 'react'; import { Todo } from '../App'; // Impor interface Todo dari App.tsx (atau types.ts) // Definisikan tipe untuk props yang diterima TodoItem interface TodoItemProps { todo: Todo; // Satu objek todo // Fungsi-fungsi handler ini akan dikirim dari App.tsx nanti onToggleComplete: (id: number) => void; onDeleteTodo: (id: number) => void; } const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggleComplete, onDeleteTodo }) => { const itemStyle: React.CSSProperties = { // Contoh tipe untuk objek style inline textDecoration: todo.isCompleted ? 'line-through' : 'none', color: todo.isCompleted ? '#a0a0a0' : '#333', opacity: todo.isCompleted ? 0.6 : 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 15px', borderBottom: '1px solid #eee', transition: 'all 0.3s ease', // Sedikit transisi }; const buttonContainerStyle: React.CSSProperties = { display: 'flex', gap: '8px', // Jarak antar tombol }; const buttonStyleBase: React.CSSProperties = { padding: '6px 10px', cursor: 'pointer', border: 'none', borderRadius: '4px', fontSize: '0.9em', transition: 'background-color 0.2s ease', }; const completeButtonStyle: React.CSSProperties = { ...buttonStyleBase, // Salin style dasar backgroundColor: todo.isCompleted ? '#6c757d' : '#28a745', // Abu-abu kalau udah selesai, hijau kalau belum color: 'white', }; const deleteButtonStyle: React.CSSProperties = { ...buttonStyleBase, backgroundColor: '#dc3545', // Merah color: 'white', }; return ( <li style={itemStyle} className="todo-item"> <span onClick={() => onToggleComplete(todo.id)} style={{ cursor: 'pointer', flexGrow: 1 }} className={todo.isCompleted ? 'completed-text' : ''} > {todo.text} </span> <div style={buttonContainerStyle}> <button onClick={() => onToggleComplete(todo.id)} style={completeButtonStyle} aria-label={todo.isCompleted ? "Tandai belum selesai" : "Tandai selesai"} > {todo.isCompleted ? 'Batal' : 'Selesai'} </button> <button onClick={() => onDeleteTodo(todo.id)} style={deleteButtonStyle} aria-label="Hapus tugas" > Hapus </button> </div> </li> ); }; export default TodoItem;
Bedah Kode TodoItem.tsx
:
- Impor
Todo
: Kita imporinterface Todo
yang udah kita bikin diApp.tsx
(atau nanti bisa dipindah ke filetypes.ts
sendiri biar lebih rapi). Ini penting biarTodoItemProps
bisa pake tipeTodo
.- Kalau
App.tsx
danTodoItem.tsx
ada di folder yang beda (TodoItem
dicomponents/
), path impornya jadiimport { Todo } from '../App';
(naik satu level daricomponents
kesrc
, baru keApp
).
- Kalau
interface TodoItemProps
: Mendefinisikan "kontrak" props:todo: Todo
: Komponen ini nerima satu objektodo
yang bentuknya harus sesuaiinterface Todo
.onToggleComplete: (id: number) => void
: Fungsi buat nandain tugas selesai/belum. Nerimaid
tugas, gak nge-return apa-apa.onDeleteTodo: (id: number) => void
: Fungsi buat ngehapus tugas.
const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggleComplete, onDeleteTodo }) => { ... }
:- Definisi functional component yang nerima props sesuai
TodoItemProps
.
- Definisi functional component yang nerima props sesuai
itemStyle
: Contoh penggunaan style inline pake objek JavaScript. PerhatiintextDecoration
dancolor
-nya berubah tergantungtodo.isCompleted
(conditional styling).React.CSSProperties
: Ini tipe bawaan React buat objek style, ngasih auto-completion buat properti CSS.
- JSX Return:
- Ngerender satu elemen
<li>
. - Teks tugas (
todo.text
) dibungkus<span>
. Pas span ini diklik, dia manggilonToggleComplete(todo.id)
. - Ada dua tombol: "Selesai/Batal" dan "Hapus". Masing-masing manggil fungsi prop yang sesuai sambil ngirim
todo.id
. aria-label
ditambahkan ke tombol untuk aksesibilitas yang lebih baik.
- Ngerender satu elemen
Langkah 2: Membuat Komponen TodoList.tsx
Komponen ini yang bakal ngurusin iterasi (looping) array todos
dan ngerender banyak TodoItem
.
-
Di folder
src/components/
, bikin file baruTodoList.tsx
. -
Isi kodenya:
File
src/components/TodoList.tsx
:tsx
import React from 'react'; import { Todo } from '../App'; // Impor interface Todo import TodoItem from './TodoItem'; // Impor komponen TodoItem interface TodoListProps { todos: Todo[]; // Array dari objek Todo onToggleComplete: (id: number) => void; onDeleteTodo: (id: number) => void; } const TodoList: React.FC<TodoListProps> = ({ todos, onToggleComplete, onDeleteTodo }) => { if (todos.length === 0) { return <p className="empty-message">Hore, tidak ada tugas! Saatnya santai. 🎉</p>; } return ( <ul className="todo-list"> {todos.map(todo => ( // Untuk tiap objek 'todo' di array 'todos', render satu komponen TodoItem <TodoItem key={todo.id} // WAJIB! Pake ID unik sebagai key todo={todo} // Kirim objek todo ini sebagai prop ke TodoItem onToggleComplete={onToggleComplete} // Teruskan fungsi handler dari App onDeleteTodo={onDeleteTodo} // Teruskan fungsi handler dari App /> ))} </ul> ); }; export default TodoList;
Bedah Kode TodoList.tsx
:
- Impor
Todo
danTodoItem
. interface TodoListProps
:todos: Todo[]
: Nerima arraytodos
yang isinya objek-objekTodo
.onToggleComplete
danonDeleteTodo
: Nerima fungsi-fungsi handler dari parent (App.tsx
) buat diterusin lagi keTodoItem
.
- Conditional Rendering: Kalau
todos.length === 0
(array-nya kosong), dia nampilin pesan. .map()
untuk Merender List:todos.map(todo => ...)
: Ini jurus andalan buat ngerender list di React! Kita nge-loop tiaptodo
di arraytodos
.- Buat tiap
todo
, kita nge-return satu komponen<TodoItem />
. key={todo.id}
: Ini SUPER PENTING. React butuhkey
unik buat tiap item di list biar dia bisa ngelola update DOM dengan efisien. Kita paketodo.id
yang udah kita pastiin unik.todo={todo}
: Kita ngirim seluruh objektodo
saat ini sebagai prop keTodoItem
.onToggleComplete={onToggleComplete}
danonDeleteTodo={onDeleteTodo}
: Kita "nerusin" fungsi handler dariApp.tsx
(yang diterimaTodoList
sebagai prop) ke tiapTodoItem
sebagai prop juga. Jadi, pas tombol diTodoItem
diklik, dia sebenernya manggil fungsi yang ada diApp.tsx
.
Langkah 3: Menggunakan TodoList
di App.tsx
Sekarang, kita update lagi src/App.tsx
buat ngimpor dan ngerender TodoList
, dan ngimplementasiin fungsi toggleComplete
serta deleteTodo
.
File src/App.tsx
(bagian yang relevan diubah/ditambah):
import React, { useState, useEffect } from 'react';
import './App.css';
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList'; // <-- IMPORT TodoList
export interface Todo {
id: number;
text: string;
isCompleted: boolean;
}
function App() {
const [todos, setTodos] = useState<Todo[]>(() => {
const savedTodos = localStorage.getItem('todos');
if (savedTodos) {
try {
return JSON.parse(savedTodos) as Todo[];
} catch (e) {
console.error("Gagal mem-parse todos dari localStorage:", e);
return [];
}
}
return [];
});
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
const addTodo = (textDariForm: string) => {
if (!textDariForm.trim()) return;
const newTodo: Todo = {
id: Date.now(),
text: textDariForm,
isCompleted: false,
};
setTodos(prevTodos => [...prevTodos, newTodo]);
};
// Fungsi buat toggle status selesai/belum tugas
const toggleComplete = (idToToggle: number) => { // Kasih tipe ke parameter id
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === idToToggle ? { ...todo, isCompleted: !todo.isCompleted } : todo
)
);
};
// Fungsi buat ngehapus tugas
const deleteTodo = (idToDelete: number) => { // Kasih tipe ke parameter id
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== idToDelete));
};
return (
<div className="app-container">
<header>
<h1>Aplikasi To-Do List Saya (React + TS)</h1>
</header>
<main>
<TodoForm onAddTodo={addTodo} />
{/* Render TodoList dan kirim props yang dibutuhkan */}
<TodoList
todos={todos}
onToggleComplete={toggleComplete}
onDeleteTodo={deleteTodo}
/>
</main>
<footer className="app-footer"> {/* Tambah class untuk styling footer */}
<p>Total Tugas: {todos.length} | Selesai: {todos.filter(t => t.isCompleted).length}</p>
</footer>
</div>
);
}
export default App;
Perubahan Penting di App.tsx
:
- Impor
TodoList
. - Implementasi
toggleComplete(idToToggle: number)
:- Pake
setTodos
dengan fungsi updater. - Dia nge-map array
todos
lama. Kalautodo.id
cocok samaidToToggle
, dia bikin objek todo baru dengan semua properti lama (...todo
) tapiisCompleted
-nya dibalik (!todo.isCompleted
). Kalau gak cocok, dia balikin objektodo
yang lama.
- Pake
- Implementasi
deleteTodo(idToDelete: number)
:- Pake
setTodos
dengan fungsi updater. - Dia nge-filter array
todos
lama, cuma nyimpen todo yangid
-nya GAK SAMA denganidToDelete
.
- Pake
- Render
TodoList
:<TodoList todos={todos} onToggleComplete={toggleComplete} onDeleteTodo={deleteTodo} />
: Kita kirim statetodos
dan dua fungsi handler baru ini sebagai props keTodoList
.
Langkah 4: Styling Tambahan (Opsional)
Kamu bisa nambahin style lagi ke src/App.css
buat class .todo-list
, .todo-item
, .completed-text
, .empty-message
, dan .app-footer
biar tampilannya makin oke. (Contoh stylingnya udah ada di TodoItem.jsx
pake inline style, dan di style-todo.css
dari materi Proyek Mini JS sebelumnya, bisa kamu adaptasi).
Coba Jalanin Lagi!
Kalau semua udah bener:
- Pastikan dev server Vite (
npm run dev
) masih jalan. - Refresh browser.
- Sekarang, kamu harusnya udah bisa:
- Nambahin tugas baru.
- Liat daftar tugasnya muncul.
- Ngeklik teks tugas atau tombol "Selesai"/"Batal" buat nandain tugas (teksnya jadi dicoret).
- Ngeklik tombol "Hapus" buat ngilangin tugas dari daftar.
- Semua perubahan juga bakal kesimpen di
localStorage
!
Hore! Aplikasi To-Do List React + TypeScript kita udah punya fungsionalitas inti! Di sini kita liat gimana:
- Komponen dipecah jadi bagian-bagian yang lebih kecil dan fokus (
App
->TodoList
->TodoItem
). - Props dipake buat ngalirin data (
todos
) dan fungsi callback (onToggleComplete
,onDeleteTodo
) dari parent ke child, sampe ke child paling dalem. - State utama (
todos
) tetep dipegang sama komponen parent bersama terdekat (App
) - ini contoh nyata Lifting State Up. - TypeScript ngebantu mastiin "kontrak" props antar komponen itu bener (tipe datanya sesuai).
Ini adalah pola arsitektur komponen yang umum banget di React. Pahami alur data dan event-nya ya!
Di bagian terakhir studi kasus, kita bisa nambahin sedikit sentuhan akhir kayak styling yang lebih konsisten atau penyempurnaan kecil lainnya.
Uji Pemahamanmu!
Memeriksa status login...
Sebelumnya
Studi Kasus To-Do: Komponen Form & Add
Selanjutnya
Studi Kasus To-Do: Fungsi Toggle & Delete