Mengelola State

Intermediate

Seiring berkembangnya aplikasi Anda, penting untuk memperhatikan bagaimana state Anda diatur dan memperhatikan bagaimana data mengalir diantara komponen-komponen yang ada. State yang redundan atau duplikat adalah sumber dari bug dikemudian hari. Dalam babak ini, Anda akan belajar bagaimana menata state dengan baik, bagaimana menjaga logika pembaruan state agar mudah dikelola, dan bagaimana Anda dapat berbagi state dengan komponen yang berjauhan.

Merespon masukan dengan state

Dalam React, Anda tidak perlu mengubah kode secara langsung untuk mengubah antar muka (UI). Misalnya, menulis baris perintah “nonaktifkan tombol ketika”, “aktifkan tombol ketika”, “tampilkan pesan sukses ketika”, dll disetiap baris. Melainkan, cukup menggambarkan antar muka yang ingin ditampilkan sebagai states visual dari komponen Anda (”state awal”, ”state mengetik”, ”state sukses”), dan kemudian memicu perubahan state sebagai respons terhadap masukan pengguna. Sekilas mirip dengan bagaimana desainer merencanakan antar muka.

Berikut contoh formulir kuis yang dibangun menggunakan React. Perhatikan bagaimana ia menggunakan variabel state status untuk menentukan apakah tombol kirim diaktifkan atau dinonaktifkan, dan apakah pesan sukses ditampilkan sebagai gantinya.

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>Itu Benar!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>Kuis Kota</h2>
      <p>
        Di kota manakah terdapat papan reklame yang mengubah udara menjadi air minum?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Anggap kode ini melakukan *request* ke jaringan.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Tebakan yang bagus tetapi jawaban salah. Silahkan coba lagi!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Siap mempelajari topik ini?

Baca Reacting to Input with State untuk belajar bagaimana mendekati interaksi dengan mindset yang didorong oleh state.

Baca Lebih Lanjut

Memilih struktur state

Mengatur struktur state dengan baik dapat membuat perbedaan antara komponen yang mudah dimodifikasi dan didebug, dan komponen yang selalu menjadi sumber kesalahan. Perlu dicatat bahwa state tidak boleh mengandung informasi yang tidak perlu atau duplikat. Karena jika ada state yang tidak perlu, mudah untuk lupa memperbarui state tersebut, yang akhirnya memperkenalkan masalah baru!

Misalnya, formulir ini memiliki variabel state fullName yang redundan:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Izinkan kami memeriksa Anda</h2>
      <label>
        Nama depan:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Nama belakang:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Tiket Anda akan diberikan kepada: <b>{fullName}</b>
      </p>
    </>
  );
}

Anda dapat menghapus dan menyederhanakan kode dengan menghitung fullName saat komponen di-render:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Izinkan kami memeriksa Anda</h2>
      <label>
        Nama depan:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Nama belakang:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Tiket Anda akan diberikan kepada: <b>{fullName}</b>
      </p>
    </>
  );
}

Sekilas seperti perubahan sepele, tetapi umumnya cara ini banyak memperbaiki bug yang ada pada aplikasi React.

Siap mempelajari topik ini?

Baca Choosing the State Structure untuk belajar cara merancang bentuk state untuk menghindari kesalahan (bugs).

Baca Lebih Lanjut

Berbagi state antar komponen

Terkadang, Anda ingin state dari dua komponen yang berbeda selalu berubah bersama. Untuk melakukannya, hapus state dari keduanya, pindahkan state tersebut ke bagian induk (parent) yang paling berdekatan, dan kemudian teruskan ke kedua komponen melalui props. Hal ini dikenal sebagai “menjunjung state” (lifting state up), dan ini adalah salah satu hal lumrah saat menulis kode React.

Pada contoh ini, dalam satu waktu hanya akan ada satu panel yang aktif. Untuk mencapainya, daripada menyimpan state aktif di setiap panel secara individu, komponen induk menyimpan state dan menentukan props untuk anak-anaknya.

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        Dengan populasi sekitar 2 juta orang, Almaty adalah kota terbesar di Kazakhstan. Dari tahun 1929 hingga 1997, kota ini menjadi ibu kota Kazakhstan.
      </Panel>
      <Panel
        title="Etymology"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        Nama "Almaty" berasal dari kata <span lang="kk-KZ">алма</span>,dalam bahasa Kazakh yang berarti "apel"dan sering diterjemahkan sebagai "penuh dengan apel". Sebenarnya, wilayah sekitar Almaty dipercaya sebagai asal usul apel, dan <i lang="la">Malus sieversii</i> liar dianggap sebagai kandidat yang mungkin menjadi nenek moyang apel domestik modern.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Tampilkan
        </button>
      )}
    </section>
  );
}

Siap mempelajari topik ini?

Baca Berbagi State Antar Komponen untuk mempelajari cara mengangkat state ke atas dan menjaga sinkronisasi antar komponen.

Baca Lebih Lanjut

Mempertahankan dan mengatur ulang state

Saat Anda me-render ulang sebuah komponen, React perlu memutuskan bagian pohon mana yang dipertahankan (dan diperbarui), serta bagian mana yang harus dibuang atau dibuat kembali dari awal. Pada kebanyakan kasus, perilaku otomatis React ini sudah cukup baik. Secara default, React akan mempertahankan bagian-bagian pohon yang “cocok” dengan struktur pohon yang sebelumnya telah di-render.

Namun, ada kalanya hal ini bukan yang Anda harapkan. Dalam contoh aplikasi obrolan ini, ketika Anda mengetik pesan dan kemudian mengubah penerima pesan, itu tidak mengatur ulang bidang masukan yang ada. Hal ini bisa membuat pengguna secara tidak sengaja mengirim pesan ke orang yang salah:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: 'taylor@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

React memungkinkan Anda untuk mengesampingkan perilaku default, dan memaksa sebuah komponen untuk mengatur ulang statusnya (state) dengan memberikan key yang berbeda, seperti <Chat key={email} />. Hal ini memberitahu React bahwa jika penerima berbeda, itu harus dianggap sebagai komponen Chat yang berbeda yang perlu dibuat kembali dari awal dengan data (dan UI seperti input) yang baru. Sekarang, beralih antara penerima mengatur ulang input - meskipun Anda me-render komponen yang sama.

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.email} contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: 'taylor@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

Siap mempelajari topik ini?

Baca Preserving and Resetting State untuk mempelajari masa hidup status dan cara mengendalikannya.

Baca Lebih Lanjut

Mengekstrak logika state ke dalam reducer

Komponen dengan banyak pembaruan state yang tersebar di banyak event handler dapat menjadi sangat membingungkan. Untuk kasus-kasus ini, Anda dapat mengkonsolidasikan semua logika pembaruan state di luar komponen Anda dalam sebuah fungsi tunggal, yang disebut ”reducer“. Event handler Anda menjadi lebih ringkas karena hanya menentukan “aksi” pengguna. Di bagian bawah file, fungsi reducer menentukan bagaimana state harus diperbarui sebagai respons terhadap setiap aksi!

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Rencana perjalanan Praha</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Mengunjungi Musium Kafka', done: true },
  { id: 1, text: 'Menonton Pertujukan Boneka', done: false },
  { id: 2, text: 'Foto Tembok Lennon', done: false }
];

Siap mempelajari topik ini?

Baca Extracting State Logic into a Reducer untuk mempelajari cara mengkonsolidasikan logika dalam fungsi reducer.

Baca Lebih Lanjut

Melewatkan data secara dalam dengan context

Biasanya, Anda akan melewatkan informasi dari komponen induk ke komponen anak (children) melalui props. Namun, melewatkan props dapat menjadi merepotkan jika Anda perlu melewatkan beberapa prop melalui banyak komponen, atau jika banyak komponen membutuhkan informasi yang sama. Context memungkinkan komponen induk membuat beberapa informasi tersedia untuk setiap komponen di bawahnya—tidak peduli seberapa dalam itu—tanpa melewatkan secara eksplisit melalui props.

Di sini, komponen Heading menentukan tingkat judulnya dengan “bertanya” pada Section terdekat untuk tingkatnya. Setiap Section melacak tingkatnya sendiri dengan bertanya pada Section induk dan menambahkan satu. Setiap Section menyediakan informasi kepada semua komponen di bawahnya tanpa melewatkan props—itu dilakukan melalui context.

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Judul</Heading>
      <Section>
        <Heading>Judul</Heading>
        <Heading>Judul</Heading>
        <Heading>Judul</Heading>
        <Section>
          <Heading>Sub-judul</Heading>
          <Heading>Sub-judul</Heading>
          <Heading>Sub-judul</Heading>
          <Section>
            <Heading>Sub-sub-judul</Heading>
            <Heading>Sub-sub-judul</Heading>
            <Heading>Sub-sub-judul</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

Siap mempelajari topik ini?

Baca Passing Data Deeply with Context untuk mempelajari penggunaan context sebagai alternatif dari melewatkan props.

Baca Lebih Lanjut

Peningkatan skala dengan reducer dan context

Reducer memungkinkan Anda mengonsolidasikan logika pembaruan state dari sebuah komponen. Context memungkinkan Anda melewatkan informasi ke komponen lain secara dalam. Anda dapat menggabungkan reducer dan context bersama-sama untuk mengelola state dari layar yang kompleks.

Dengan pendekatan ini, sebuah komponen induk dengan state yang kompleks dikelola dengan reducer. Komponen lain di dalam tree dapat membaca state-nya melalui context. Mereka juga dapat melakukan dispatch tindakan untuk memperbarui state tersebut.

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Hari libur di Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

Siap mempelajari topik ini?

Baca Peningkatan Skala dengan Reducer dan Context untuk mempelajari bagaimana pengelolaan state mengembang pada aplikasi yang berkembang.

Baca Lebih Lanjut

Apa selanjutnya?

Lanjut ke halaman Reacting to Input with State untuk mulai membaca bab ini halaman per halaman!

Atau, jika Anda sudah familiar dengan topik-topik ini, mengapa tidak membaca tentang Escape Hatches?