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.
Kuis Komponen List & Item To-Do (React+TS)
Pertanyaan 1 dari 4
Di komponen `TodoList.tsx`, mengapa penting untuk memberikan `key` prop yang unik pada setiap komponen `<TodoItem />` saat melakukan iterasi dengan `.map()`?