ReactJS

Belum masuk. Progresmu tidak tersimpan.

Masuk
  • Bab 8 dari 10
  • 30 menit
  • 1 latihan kode
  • 4 soal kuis

Form, Komunikasi Antar Komponen, dan Mengambil Data

Di bab ini (9 bagian)
  1. Tujuan
  2. Controlled component: state jadi bos
  3. Banyak input, satu state object
  4. Validasi sederhana
  5. Komunikasi antar komponen
  6. Mengambil data dari API
  7. Sekilas TanStack Query
  8. Kesalahan umum
  9. Rangkuman

Tujuan bab ini

  • Membuat form dengan controlled component: value dari state, perubahan lewat onChange.
  • Mengelola banyak input dengan satu state object dan menambahkan validasi sederhana.
  • Mengalirkan data antar komponen: props turun, callback naik, dan lifting state up.
  • Mengambil data dari API di useEffect dengan state loading, error, dan data, lengkap dengan cleanup AbortController.

Controlled component: state jadi bos

Di HTML biasa, input menyimpan isinya sendiri. Kamu baru membacanya saat butuh, misalnya waktu form dikirim. Di React, kita biasanya membalik hubungan itu: state React yang memegang nilai, input hanya menampilkannya. Inilah yang disebut controlled component.

import { useState } from 'react';

function FormNama() {
  const [nama, setNama] = useState('');

  return (
    <>
      <input value={nama} onChange={(e) => setNama(e.target.value)} />
      <p>Halo, {nama || 'tamu'}!</p>
    </>
  );
}

Alurnya langkah demi langkah:

  1. Kamu mengetik huruf "B". Browser memicu event change (di React, onChange jalan di setiap ketukan, mirip event input di DOM).
  2. Handler memanggil setNama('B').
  3. React render ulang, value={nama} sekarang 'B', jadi input menampilkan "B".

Analoginya, input itu layar TV dan state adalah siaran. Layar tidak bisa menampilkan apa pun selain yang disiarkan. Untungnya, karena nilai ada di state, kamu bebas mengolahnya: mengubah ke huruf kapital, membatasi panjang, mengosongkan form setelah kirim, atau mengisi otomatis dari data yang sudah ada.

Kalau kamu memasang value tanpa onChange, input jadi tidak bisa diketik dan React memberi peringatan di console. Untuk nilai awal tanpa kontrol penuh, pakai defaultValue (uncontrolled), tapi di kelas ini kita fokus ke versi controlled.

Banyak input, satu state object

Form pendaftaran bisa punya lima field. Membuat lima useState dan lima handler itu melelahkan. Triknya: simpan semua field dalam satu objek, beri setiap input atribut name yang sama dengan kunci objek, lalu pakai computed property [name].

function FormPendaftaran() {
  const [form, setForm] = useState({ nama: '', email: '', setuju: false });

  function handleChange(e) {
    const { name, value, type, checked } = e.target;
    setForm({ ...form, [name]: type === 'checkbox' ? checked : value });
  }

  function handleSubmit(e) {
    e.preventDefault(); // cegah halaman reload
    console.log(form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="nama" value={form.nama} onChange={handleChange} />
      <input name="email" type="email" value={form.email} onChange={handleChange} />
      <label>
        <input name="setuju" type="checkbox" checked={form.setuju} onChange={handleChange} />
        Saya setuju
      </label>
      <button type="submit">Daftar</button>
    </form>
  );
}
// Setelah mengetik dan klik Daftar, console:
// { nama: 'Sinta', email: 'sinta@mail.id', setuju: true }

Dua hal penting di sini. Pertama, ...form menyalin semua field lama sebelum menimpa satu field. Tanpa spread, objek baru hanya berisi field yang sedang diketik dan field lain hilang. Kedua, checkbox memakai checked, bukan value.

Validasi sederhana

Karena semua nilai ada di state, validasi cukup berupa fungsi murni yang menerima objek form dan mengembalikan objek error. Fungsi seperti ini gampang diuji tanpa browser.

function validasi(form) {
  const error = {};
  if (form.nama.trim().length < 3) error.nama = 'Nama minimal 3 huruf';
  if (!form.email.includes('@')) error.email = 'Email tidak valid';
  return error;
}

// di dalam komponen
const error = validasi(form);
const bisaKirim = Object.keys(error).length === 0;

<button type="submit" disabled={!bisaKirim}>Daftar</button>
{error.nama && <small>{error.nama}</small>}

Perhatikan error tidak disimpan di state. Ia dihitung dari state setiap render, jadi tidak mungkin basi. Untuk form besar dengan banyak aturan, pustaka seperti React Hook Form atau skema Zod membuat kodenya lebih ringkas, tapi idenya tetap sama.

Komunikasi antar komponen

Data di React mengalir satu arah: dari induk ke anak lewat props. Lalu bagaimana anak mengubah data induk? Induk mengirim fungsi sebagai prop, dan anak memanggilnya. Props turun, callback naik.

Lifting state up: props turun, callback naik

Bayangkan dua komponen saudara: Masukan tempat mengetik dan Tampilan yang menampilkan teks. Saudara tidak bisa saling mengirim data langsung. Solusinya, angkat state ke induk terdekat keduanya (lifting state up).

function Induk() {
  const [pesan, setPesan] = useState('');
  return (
    <>
      <Masukan nilai={pesan} onKirim={setPesan} />
      <Tampilan teks={pesan} />
    </>
  );
}

function Masukan({ nilai, onKirim }) {
  return <input value={nilai} onChange={(e) => onKirim(e.target.value)} />;
}

function Tampilan({ teks }) {
  return <p>Kamu menulis: {teks}</p>;
}

Konvensi namanya: prop berisi fungsi diawali on, misalnya onKirim, onHapus, onPilih, persis seperti onClick bawaan. Kalau props harus dioper melewati lima lapisan komponen, itu tanda saatnya memakai Context, yang dibahas di bab berikutnya.

Mengambil data dari API

Mengambil data adalah efek samping: terjadi di luar proses render. Tempatnya di useEffect. Setiap pengambilan data punya tiga kondisi yang wajib ditangani: loading, error, dan sukses (termasuk sukses tapi kosong).

import { useEffect, useState } from 'react';

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

  useEffect(() => {
    const controller = new AbortController();

    async function ambilData() {
      try {
        const res = await fetch('/api/artikel', { signal: controller.signal });
        if (!res.ok) throw new Error(`Gagal memuat (${res.status})`);
        setData(await res.json());
      } catch (err) {
        if (!controller.signal.aborted) setError(err.message);
      } finally {
        if (!controller.signal.aborted) setLoading(false);
      }
    }

    ambilData();
    return () => controller.abort(); // cleanup
  }, []);

  if (loading) return <p>Memuat data...</p>;
  if (error) return <p>Terjadi kesalahan: {error}</p>;
  if (data.length === 0) return <p>Belum ada artikel.</p>;
  return <ul>{data.map((a) => <li key={a.id}>{a.judul}</li>)}</ul>;
}

Kenapa kodenya begitu?

  • Fungsi async di dalam efek. Fungsi yang dikirim ke useEffect tidak boleh async, karena fungsi async selalu mengembalikan Promise, sedangkan React mengharapkan fungsi cleanup atau tidak ada apa-apa. Jadi kita bikin ambilData async di dalamnya lalu memanggilnya.
  • res.ok. fetch tidak melempar error untuk status 404 atau 500. Ia hanya gagal kalau jaringan putus. Status harus kamu cek sendiri.
  • AbortController. Kalau pengguna pindah halaman sebelum data datang, komponen di-unmount dan cleanup membatalkan request. Ini juga mencegah race condition: saat efek bergantung pada id yang berganti cepat, respons lama tidak menimpa yang baru. Pengecekan signal.aborted memastikan request yang sudah dibatalkan tidak lagi mengubah state. Di Strict Mode saat development, React sengaja menjalankan efek, cleanup, lalu efek lagi, jadi request pertama memang di-abort. Itu normal.

Sekilas TanStack Query

Menulis tiga state dan efek di setiap komponen lama-lama repetitif, dan belum ada caching, retry, atau refetch saat tab dibuka lagi. Untuk proyek nyata, banyak tim memakai TanStack Query (versi 5):

import { useQuery } from '@tanstack/react-query';

function DaftarArtikel() {
  const { data, isPending, error } = useQuery({
    queryKey: ['artikel'],
    queryFn: () => fetch('/api/artikel').then((r) => r.json()),
  });

  if (isPending) return <p>Memuat data...</p>;
  if (error) return <p>Terjadi kesalahan: {error.message}</p>;
  return <ul>{data.map((a) => <li key={a.id}>{a.judul}</li>)}</ul>;
}

Aplikasinya dibungkus sekali dengan QueryClientProvider. Komponen lain yang memakai queryKey sama langsung mendapat data dari cache. Pahami dulu versi useEffect supaya kamu tahu apa yang dikerjakan pustaka ini di balik layar.

Kesalahan umum

  • Lupa spread saat update objek. setForm({ [name]: value }) menghapus field lain.
  • Lupa e.preventDefault() di submit. Halaman reload dan state hilang.
  • Menulis useEffect(async () => ...). Bikin fungsi async di dalam efek, lalu panggil.
  • Hanya menangani kondisi sukses. Pengguna melihat layar kosong saat loading atau error.
  • Menganggap fetch melempar error untuk 404. Selalu cek res.ok.

Rangkuman

  • Controlled component: value dari state, onChange memperbarui state, state jadi satu-satunya sumber kebenaran.
  • Satu state object plus [name] dan spread melayani banyak input dengan satu handler.
  • Validasi sebaiknya fungsi murni yang dihitung dari state setiap render.
  • Props turun, callback naik; state yang dipakai bersama diangkat ke induk terdekat.
  • Ambil data di useEffect dengan fungsi async di dalamnya, tangani loading, error, dan sukses, lalu batalkan dengan AbortController di cleanup.
  • TanStack Query menambahkan caching dan retry untuk data server.

Selesaikan bab ini

  1. 1. Baca materi
  2. 2. Latihan kode (1)
  3. 3. Kuis (4 soal)

Latihan kode: Logika form pendaftaran dan pengambilan data

Tiga fungsi murni ini dipakai komponen form dan daftar artikel. Di komponen, handleChange memanggil setForm(ubahField(form, name, value)), error dihitung dengan validasiPendaftaran(form) setiap render, dan useEffect memanggil muatArtikel(fetch) lalu menyimpan hasilnya ke state.

  • ubahField(form, name, value) mengembalikan objek baru berisi semua field lama, dengan field name diganti value. Objek asli tidak boleh berubah.
  • validasiPendaftaran(form) mengembalikan objek error. Bila nama (setelah trim) kurang dari 3 huruf, isi nama: 'Nama minimal 3 huruf'. Bila email tidak mengandung @, isi email: 'Email tidak valid'. Form yang valid menghasilkan {}.
  • muatArtikel(ambil) adalah fungsi async. Panggil await ambil('/api/artikel'). Bila res.ok false, anggap gagal dengan pesan 'Gagal memuat (<status>)'. Bila sukses, kembalikan { data: <hasil res.json()>, error: null }. Bila gagal atau ambil melempar error, kembalikan { data: [], error: <pesan error> }.

Tiga API palsu sudah disiapkan: apiSukses, apiGagal (status 500), dan apiPutus (melempar Error('Jaringan putus')).

Cek pemahaman

  1. 1Apa yang terjadi bila sebuah input diberi value={nama} tetapi tidak diberi onChange?

  2. 2Apa output kode berikut? Kode ini meniru handleChange sebuah form.

    let form = { nama: 'Budi', email: '' };
    function setForm(baru) { form = baru; }
    
    function handleChange(e) {
      const { name, value } = e.target;
      setForm({ [name]: value });
    }
    
    handleChange({ target: { name: 'email', value: 'a@b.id' } });
    console.log(JSON.stringify(form));
  3. 3Fungsi efek boleh langsung ditulis async, misalnya useEffect(async () => { ... }, []).

  4. 4Komponen Masukan dan Tampilan bersaudara dan butuh teks yang sama. Di mana state teks sebaiknya disimpan?