Langsung ke konten
KamusNgoding
Pemula React 3 menit baca

State dan Hooks di React: Membuat UI yang Interaktif

#react #state #hooks #useState #useEffect #useRef #immutability
📚

Baca dulu sebelum ini:

Komponen yang hanya menampilkan props tidak cukup untuk aplikasi nyata — kamu perlu UI yang bisa berubah berdasarkan interaksi pengguna. Di sinilah state berperan. State adalah data yang “dimiliki” oleh komponen dan menyebabkan komponen re-render saat nilainya berubah. Hooks adalah fungsi khusus React yang memberi komponen fungsi akses ke fitur-fitur React — useState dan useEffect adalah yang paling penting.

useState — Mengelola State Sederhana

import { useState } from 'react';

function Penghitung() {
    // useState mengembalikan [nilaiSaat ini, fungsiUpdate]
    const [hitung, setHitung] = useState(0); // 0 = nilai awal

    return (
        <div>
            <p>Hitungan: {hitung}</p>
            <button onClick={() => setHitung(hitung + 1)}>+ Tambah</button>
            <button onClick={() => setHitung(hitung - 1)}>- Kurang</button>
            <button onClick={() => setHitung(0)}>Reset</button>
        </div>
    );
}

Aturan penting: Jangan pernah mengubah state secara langsung (hitung = 5). Selalu gunakan fungsi setter (setHitung(5)). Hanya dengan setter React tahu perlu melakukan re-render.

State dan Re-render

function StatusLogin() {
    const [isLogin, setIsLogin] = useState(false);
    const [nama, setNama] = useState("Tamu");

    const handleLogin = () => {
        setIsLogin(true);
        setNama("Ali Akbar");
    };

    const handleLogout = () => {
        setIsLogin(false);
        setNama("Tamu");
    };

    return (
        <div>
            <p>Status: {isLogin ? "✅ Login" : "❌ Belum login"}</p>
            <p>Pengguna: {nama}</p>
            {isLogin
                ? <button onClick={handleLogout}>Logout</button>
                : <button onClick={handleLogin}>Login</button>
            }
        </div>
    );
}

Immutability — Cara Update Objek dan Array

Saat state berupa objek atau array, kamu tidak boleh mengubahnya langsung:

function FormPengguna() {
    const [pengguna, setPengguna] = useState({
        nama: "",
        email: "",
        kota: ""
    });

    const handleUbah = (field, nilai) => {
        // ❌ SALAH — jangan mutasi state langsung
        // pengguna.nama = nilai;
        // setPengguna(pengguna);

        // ✅ BENAR — buat objek baru dengan spread operator
        setPengguna({ ...pengguna, [field]: nilai });
    };

    return (
        <form>
            <input
                value={pengguna.nama}
                onChange={e => handleUbah("nama", e.target.value)}
                placeholder="Nama"
            />
            <input
                value={pengguna.email}
                onChange={e => handleUbah("email", e.target.value)}
                placeholder="Email"
            />
            <p>Data: {JSON.stringify(pengguna)}</p>
        </form>
    );
}

Update Array dalam State

function DaftarTugas() {
    const [tugas, setTugas] = useState([
        { id: 1, teks: "Belajar React", selesai: false },
        { id: 2, teks: "Membuat project", selesai: false },
    ]);
    const [inputBaru, setInputBaru] = useState("");

    // Tambah elemen — spread + elemen baru
    const tambahTugas = () => {
        if (!inputBaru.trim()) return;
        setTugas([...tugas, { id: Date.now(), teks: inputBaru, selesai: false }]);
        setInputBaru("");
    };

    // Toggle selesai — map menghasilkan array baru
    const toggleSelesai = (id) => {
        setTugas(tugas.map(t =>
            t.id === id ? { ...t, selesai: !t.selesai } : t
        ));
    };

    // Hapus — filter menghasilkan array baru
    const hapus = (id) => {
        setTugas(tugas.filter(t => t.id !== id));
    };

    return (
        <div>
            <div>
                <input
                    value={inputBaru}
                    onChange={e => setInputBaru(e.target.value)}
                    onKeyDown={e => e.key === "Enter" && tambahTugas()}
                    placeholder="Tugas baru..."
                />
                <button onClick={tambahTugas}>Tambah</button>
            </div>
            <ul>
                {tugas.map(t => (
                    <li key={t.id} style={{ textDecoration: t.selesai ? "line-through" : "none" }}>
                        <input
                            type="checkbox"
                            checked={t.selesai}
                            onChange={() => toggleSelesai(t.id)}
                        />
                        {t.teks}
                        <button onClick={() => hapus(t.id)}>🗑</button>
                    </li>
                ))}
            </ul>
            <p>{tugas.filter(t => t.selesai).length}/{tugas.length} selesai</p>
        </div>
    );
}

useEffect — Side Effects

useEffect digunakan untuk operasi di luar render: fetch data, subscribe event, update title, set timer:

import { useState, useEffect } from 'react';

function JudulDinamis({ halaman }) {
    useEffect(() => {
        // Dijalankan setelah setiap render (atau saat dependency berubah)
        document.title = `${halaman} — KamusNgoding`;

        // Cleanup function — dijalankan sebelum effect berikutnya atau saat unmount
        return () => {
            document.title = "KamusNgoding";
        };
    }, [halaman]); // Dependency array: effect hanya jalan saat 'halaman' berubah

    return <h1>{halaman}</h1>;
}

Fetch Data dengan useEffect

function DaftarArtikel() {
    const [artikel, setArtikel] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        // Jalankan sekali saat komponen pertama mount ([] = array kosong)
        const ambilData = async () => {
            try {
                setLoading(true);
                const response = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
                if (!response.ok) throw new Error("Gagal mengambil data");
                const data = await response.json();
                setArtikel(data);
            } catch (err) {
                setError(err.message);
            } finally {
                setLoading(false);
            }
        };

        ambilData();
    }, []); // [] = hanya jalan sekali saat mount

    if (loading) return <p>Memuat data...</p>;
    if (error) return <p style={{ color: "red" }}>Error: {error}</p>;

    return (
        <ul>
            {artikel.map(a => (
                <li key={a.id}>{a.title}</li>
            ))}
        </ul>
    );
}

Tiga Bentuk Dependency Array

// 1. Tanpa dependency array — jalan setiap render
useEffect(() => {
    console.log("Render terjadi!");
});

// 2. Array kosong [] — jalan sekali saat mount
useEffect(() => {
    console.log("Komponen di-mount!");
    return () => console.log("Komponen di-unmount!");
}, []);

// 3. Dengan dependencies — jalan saat nilai berubah
useEffect(() => {
    console.log(`Pencarian: ${query}`);
    // Lakukan pencarian...
}, [query]); // Jalan saat 'query' berubah

useRef — Referensi yang Tidak Menyebabkan Re-render

import { useState, useRef } from 'react';

function Stopwatch() {
    const [waktu, setWaktu] = useState(0);
    const [berjalan, setBerjalan] = useState(false);
    const intervalRef = useRef(null); // Menyimpan ID interval tanpa trigger re-render

    const mulai = () => {
        if (berjalan) return;
        setBerjalan(true);
        intervalRef.current = setInterval(() => {
            setWaktu(w => w + 1); // Functional update untuk state yang bergantung nilai sebelumnya
        }, 1000);
    };

    const stop = () => {
        clearInterval(intervalRef.current);
        setBerjalan(false);
    };

    const reset = () => {
        stop();
        setWaktu(0);
    };

    return (
        <div>
            <p style={{ fontSize: "2rem" }}>{waktu} detik</p>
            <button onClick={mulai} disabled={berjalan}>▶ Mulai</button>
            <button onClick={stop} disabled={!berjalan}>⏸ Stop</button>
            <button onClick={reset}>↺ Reset</button>
        </div>
    );
}

Rules of Hooks

// ✅ BENAR — panggil hooks di level atas komponen
function KomponenBenar() {
    const [nilai, setNilai] = useState(0);
    useEffect(() => { /* ... */ }, [nilai]);
    return <div>{nilai}</div>;
}

// ❌ SALAH — jangan panggil hooks dalam kondisi
function KomponenSalah({ aktif }) {
    if (aktif) {
        const [nilai, setNilai] = useState(0); // ❌ Hooks tidak boleh dalam kondisi!
    }
    return <div />;
}

Pertanyaan yang Sering Diajukan

Mengapa tidak boleh mengubah state langsung?

React menggunakan referensi objek untuk mendeteksi perubahan. Jika kamu memutasi objek/array yang sudah ada (arr.push(x)), referensinya tidak berubah — React tidak mendeteksi perubahan dan tidak melakukan re-render. Dengan membuat objek/array baru ([...arr, x]), referensi berubah dan React tahu perlu render ulang.

Apa perbedaan setNilai(n + 1) dan setNilai(n => n + 1)?

setNilai(n + 1) menggunakan nilai n saat fungsi didefinisikan — bisa stale (kadaluarsa) jika ada banyak update berurutan. setNilai(n => n + 1) menggunakan nilai terbaru sebagai argumen — selalu akurat. Gunakan bentuk fungsi saat update bergantung pada nilai state sebelumnya, terutama dalam interval atau event handler async.

Kapan useEffect perlu cleanup function?

Kapan pun useEffect membuat sesuatu yang perlu “dibersihkan”: (1) setInterval/setTimeout — harus di-clearInterval/clearTimeout, (2) event listener — harus di-removeEventListener, (3) WebSocket/subscription — harus disconnect/unsubscribe, (4) request API — bisa di-abort. Tanpa cleanup, bisa terjadi memory leak atau bug saat komponen di-unmount.

Apa itu “stale closure” dalam hooks?

Stale closure terjadi saat sebuah fungsi “mengingat” nilai lama dari state karena dibuat sebelum state diperbarui. Solusinya: gunakan functional update setState(prev => ...) untuk state, atau tambahkan state ke dependency array useEffect. Eslint plugin react-hooks/exhaustive-deps membantu mendeteksi dependency yang hilang.

Kesimpulan

HookKegunaanContoh
useStateMenyimpan state lokalconst [n, setN] = useState(0)
useEffectSide effects & lifecycleuseEffect(() => {...}, [deps])
useRefReferensi tanpa re-renderref.current = value

Artikel sebelumnya: Komponen dan Props di React — membangun UI reusable.

Langkah selanjutnya: Event Handling dan Form di React — cara menangani interaksi pengguna dan membuat form yang lengkap.

Artikel Terkait