K

Command Palette

Search for a command to run...

Daftar

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:

  1. TodoItem.tsx: Komponen kecil yang tugasnya nampilin satu item tugas individual. Dia bakal nerima data satu tugas (ID, teks, status selesai) sebagai props.
  2. TodoList.tsx: Komponen yang tugasnya ngerender keseluruhan daftar tugas. Dia bakal nerima array todos dari App.tsx sebagai prop, terus nge-map array itu jadi sekumpulan komponen TodoItem.

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.

  1. Di folder src/components/, bikin file baru TodoItem.tsx.

  2. 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 impor interface Todo yang udah kita bikin di App.tsx (atau nanti bisa dipindah ke file types.ts sendiri biar lebih rapi). Ini penting biar TodoItemProps bisa pake tipe Todo.
    • Kalau App.tsx dan TodoItem.tsx ada di folder yang beda (TodoItem di components/), path impornya jadi import { Todo } from '../App'; (naik satu level dari components ke src, baru ke App).
  • interface TodoItemProps: Mendefinisikan "kontrak" props:
    • todo: Todo: Komponen ini nerima satu objek todo yang bentuknya harus sesuai interface Todo.
    • onToggleComplete: (id: number) => void: Fungsi buat nandain tugas selesai/belum. Nerima id 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.
  • itemStyle: Contoh penggunaan style inline pake objek JavaScript. Perhatiin textDecoration dan color-nya berubah tergantung todo.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 manggil onToggleComplete(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.

Langkah 2: Membuat Komponen TodoList.tsx

Komponen ini yang bakal ngurusin iterasi (looping) array todos dan ngerender banyak TodoItem.

  1. Di folder src/components/, bikin file baru TodoList.tsx.

  2. 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 dan TodoItem.
  • interface TodoListProps:
    • todos: Todo[]: Nerima array todos yang isinya objek-objek Todo.
    • onToggleComplete dan onDeleteTodo: Nerima fungsi-fungsi handler dari parent (App.tsx) buat diterusin lagi ke TodoItem.
  • 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 tiap todo di array todos.
    • Buat tiap todo, kita nge-return satu komponen <TodoItem />.
    • key={todo.id}: Ini SUPER PENTING. React butuh key unik buat tiap item di list biar dia bisa ngelola update DOM dengan efisien. Kita pake todo.id yang udah kita pastiin unik.
    • todo={todo}: Kita ngirim seluruh objek todo saat ini sebagai prop ke TodoItem.
    • onToggleComplete={onToggleComplete} dan onDeleteTodo={onDeleteTodo}: Kita "nerusin" fungsi handler dari App.tsx (yang diterima TodoList sebagai prop) ke tiap TodoItem sebagai prop juga. Jadi, pas tombol di TodoItem diklik, dia sebenernya manggil fungsi yang ada di App.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):

tsx
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:

  1. Impor TodoList.
  2. Implementasi toggleComplete(idToToggle: number):
    • Pake setTodos dengan fungsi updater.
    • Dia nge-map array todos lama. Kalau todo.id cocok sama idToToggle, dia bikin objek todo baru dengan semua properti lama (...todo) tapi isCompleted-nya dibalik (!todo.isCompleted). Kalau gak cocok, dia balikin objek todo yang lama.
  3. Implementasi deleteTodo(idToDelete: number):
    • Pake setTodos dengan fungsi updater.
    • Dia nge-filter array todos lama, cuma nyimpen todo yang id-nya GAK SAMA dengan idToDelete.
  4. Render TodoList:
    • <TodoList todos={todos} onToggleComplete={toggleComplete} onDeleteTodo={deleteTodo} />: Kita kirim state todos dan dua fungsi handler baru ini sebagai props ke TodoList.

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:

  1. Pastikan dev server Vite (npm run dev) masih jalan.
  2. Refresh browser.
  3. 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()`?