Sinkronisasi dengan Effects

Beberapa komponen perlu melakukan sinkronisasi dengan sistem eksternal. Misalkan, Anda mungkin ingin mengontrol komponen di luar React berdasarkan state React, mengatur koneksi server, atau mengirim log analitik ketika sebuah komponen muncul di layar. Effects memungkinkan Anda menjalankan kode setelah render sehingga Anda bisa melakukan sinkronisasi dengan sistem di luar React.

Anda akan mempelajari

  • Apa itu Effect
  • Perbedaan Effect dengan event
  • Cara mendeklarasikan Effect di dalam komponen
  • How to skip re-running an Effect unnecessarily
  • Mengapa Effect berjalan dua kali di pengembangan (development) dan cara memperbaikinya

Apa itu Effect dan apa perbedaanya dengan event?

Sebelum kita membahas Effect, Anda perlu mengenal dua tipe logika di dalam komponen React:

  • Kode pe-render-an (diperkenalkan di Menggambarkan Antarmuka Pengguna) berada di tingkat atas komponen Anda. Inilah di mana Anda mengambil props dan state, mentransformasinya, dan mengembalikan JSX yang diinginkan di layar. Kode pe-render-an haruslah murni. Seperti rumus matematika, ia harus menghitung hasilnya saja, tapi tidak melakukan hal lainnya.

  • Event handlers (diperkenalkan di Menambahkan Interaktivitas) adalah fungsi bersarang di dalam komponen yang melakukan berbagai hal dan bukan hanya menghitungnya. Sebuah event handler dapat memperbarui bidang input, mengirimkan permintaan HTTP POST untuk membeli produk, atau menavigasi pengguna ke layar lain. Event handlers memiliki “efek samping” (yaitu mengubah state program) yang dihasilkan dari aksi pengguna tertentu (misalnya, tekanan tombol atau ketikan).

Terkadang hal-hal ini tidak cukup. Bayangkan sebuah komponen ChatRoom yang harus melakukan koneksi ke server obrolan (chat) ketika ditampilkan di layar. Melakukan koneksi ke server bukanlah penghitungan murni (melainkan efek samping) jadi tidak dapat dilakukan saat proses render. Meskipun itu, tidak ada event tertentu seperti klik yang akan menampilkan ChatRoom.

Effects memungkinkan Anda menentukan efek samping yang disebabkan oleh pe-render-an itu sendiri, dan bukan oleh event tertentu. Mengirim pesan di ruang obrolan merupakan event karena disebabkan secara langsung oleh pengguna yang mengeklik tombol tertentu. Namun, melakukan koneksi server merupakan Effect karena harus terjadi tanpa peduli interaksi apapun yang menyebabkan komponen ditampilkan. Effects berjalan di akhir commit setelah layar diperbarui. Ini merupakan waktu yang tepat untuk menyinkronkan komponen React dengan sistem eksternal (seperti jaringan atau pustaka pihak ketiga).

Catatan

Di sini dan selanjutnya dalam teks ini, kata “Effect” yang dikapitalisasi mengacu kepada definisi khusus React yang dijelaskan di atas, seperti efek samping yang disebabkan oleh proses render. Untuk mengacu kepada konsep pemrograman secara keseluruhan, kita akan menggunakan kata “efek samping”.

Anda mungkin tidak membutuhkan Effect

Jangan terburu-buru menambahkan Effects ke dalam komponen Anda. Perlu diingat bahwa Effects umumnya digunakan untuk “melangkah ke luar” dari kode React Anda dan menyinkronkan dengan sistem eksternal. Hal ini termasuk API peramban (browser), widget pihak ketiga, jaringan, dan lainnya. Apabila Effect Anda hanya mengatur state berdasarkan state lain, Anda mungkin tidak membutuhkan Effect.

Cara menulis Effect

Untuk menulis Effect, ikuti tiga langkah berikut:

  1. Deklarasikan Effect. Secara bawaan, Effect Anda akan berjalan setiap render.
  2. Tentukan dependensi dari Effect. Kebanyakan Effect hanya perlu dijalankan ulang ketika diperlukan, bukan setiap render. Misalnya, animasi fade-in seharusnya hanya dijalankan ketika sebuah komponen muncul. Menghubungkan dan memutuskan koneksi ke ruang obrolan seharusnya hanya terjadi ketika komponen muncul dan menghilang, atau ketika ruang obrolan berubah. Anda akan belajar cara mengontrolnya dengan menentukan dependensi.
  3. Tambahkan pembersihan (cleanup) jika diperlukan. Beberapa Effect perlu menentukan cara menghentikan, membatalkan, atau membersihkan apa pun yang sedang dilakukan. Misalnya, “sambungkan koneksi” membutuhkan “lepaskan koneksi”, “berlangganan” memerlukan “hentikan langganan”, dan “fetch” membutuhkan “batal” atau “abaikan”. Anda akan belajar cara melakukan hal tersebut dengan mengembalikan fungsi pembersihan.

Mari kita lihat langkah-langkah berikut secara detil.

Langkah 1: Deklarasikan Effect

Untuk mendeklarasikan Effect di dalam komponen, impor Hook useEffect dari React:

import { useEffect } from 'react';

Kemudian, panggil Hook tersebut di atas komponen Anda dan isikan Effect tersebut dengan kode:

function MyComponent() {
useEffect(() => {
// Kode di dalam blok ini akan dijalankan setelah *setiap* render
});
return <div />;
}

Setiap kali setelah komponen Anda di-render, React akan memperbarui layar kemudian menjalankan kode di dalam useEffect. Dengan kata lain, useEffect “menunda” sepotong kode agar tidak berjalan sampai render tersebut ditampilkan di layar.

Mari kita lihat bagaimana Anda dapat menggunakan Effect untuk melakukan sinkronisasi dengan sistem eksternal. Bayangkan sebuah komponen React <VideoPlayer>. Akan lebih baik jika kita dapat mengontrol apakah video sedang diputar atau dijeda dengan mengoper prop isPlaying ke dalamnya:

<VideoPlayer isPlaying={isPlaying} />;

Komponen VideoPlayer kustom Anda me-render tag bawaan peramban <video>:

function VideoPlayer({ src, isPlaying }) {
// TODO: lakukan sesuatu dengan isPlaying
return <video src={src} />;
}

Namun, tag <video> pada peramban tidak memiliki prop isPlaying. Satu-satunya cara untuk mengontrolnya adalah untuk memanggil metode play() dan pause() dalam elemen DOM secara manual. Anda perlu menyinkronkan nilai prop isPlaying, yang memberitahu apakah video seharusnya sedang diputar, dengan panggilan metode seperti play() dan pause().

Pertama-tama, kita perlu mendapatkan ref ke simpul DOM <video>.

Anda mungkin tergoda untuk mencoba memanggil play() atau pause() saat pe-render-an, tapi ini tidak benar:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  if (isPlaying) {
    ref.current.play();  // Memanggil ini saat rendering tidak diperbolehlkan.
  } else {
    ref.current.pause(); // Ini juga akan menyebabkan *crash*.
  }

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Alasan kode ini tidak benar adalah ia mencoba melakukan sesuati dengan simpul DOM saat proses render. Dalam React, proses render harus merupakan penghitungan murni dari JSX dan tidak boleh mengandung efek samping seperti memodifikasi DOM.

Lebih dari itu, ketika VideoPlayer dipanggil pertama kalinya, DOM belum tersedia! Belum ada simpul DOM untuk memanggil play() atau pause(), karena React tidak mengetahui DOM apa yang perlu dibuat sampai Anda mengembalikan JSX.

Solusinya adalah membungkus efek samping dengan useEffect memindahkannya keluar dari penghitungan proses render:

import { useEffect, useRef } from 'react';

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);

useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});

return <video ref={ref} src={src} loop playsInline />;
}

Dengan membungkus pembaruan DOM di dalam Effect, Anda memungkinkan React memperbarui layar terlebih dahulu. Kemudian Effect Anda dijalankan.

Ketika komponen VideoPlayer Anda di-render (baik pertama kalinya atau ketika di-render ulang), beberapa hal akan terjadi. Pertama, React akan memperbarui layar, memastikan tag <video> berada di dalam DOM dengan props yang benar. Kemudian React akan menjalankan Effect Anda. Pada akhirnya, Effect Anda akan memanggil play() atau pause() berdasarkan nilai dari isPlaying.

Coba tekan Putar/Jeda beberapa kali dan lihat bagaimana pemutar video tetap tersinkron dengan nilai isPlaying:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Jeda' : 'Putar'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Dalam contoh ini, “sistem eksternal” yang Anda sinkronisasi ke state React adalah API media peramban. Anda dapat menggunakan pendekatan sama untuk membungkus kode lama di luar React (seperti plugin jQuery) ke komponen deklaratif React.

Perlu dicatat bahwa mengontrol pemutar video jauh lebih kompleks dalam praktiknya. Memanggil play() bisa gagal, pengguna dapat memutar atau menjeda menggunakan kontrol peramban bawaan, dan sebagainya. Contoh ini sangat disederhanakan dan tidak lengkap.

Sandungan

Secara bawaan, Effects dijalankan setelah setiap render. Inilah sebabnya mengapa kode seperti ini akan menghasilkan perulangan tak terbatas (infinite loop):

const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});

Effects dijalankan sebagai hasil rendering. Mengatur state memicu rendering. Mengatur state secara langsung dalam suatu Effect, seperti mencolokkan stopkontak ke stopkontak itu sendiri. Effect berjalan, mengatur state, yang menyebabkan render ulang, yang menyebabkan Effect berjalan, mengatur state lagi, yang menyebabkan render ulang, dan seterusnya.

Perlu diingat bahwa Effects umumnya digunakan untuk “melangkah ke luar” dari kode React Anda dan menyinkronkan dengan sistem eksternal. Hal ini termasuk API peramban (browser), widget pihak ketiga, jaringan, dan lainnya. Apabila Effect Anda hanya mengatur state berdasarkan state lain, Anda mungkin tidak membutuhkan Effect.

Effects biasanya hanya digunakan untuk menyinkronkan komponen Anda dengan sistem eksternal. Jika tidak ada sistem eksternal dan Anda hanya ingin mengatur state berdasarkan state lain, Anda mungkin tidak membutuhkan Effect.

Langkah 2: Tentukan dependensi dari Effect

Secara bawaan, Effects berjalan setelah setiap render. Seringkali, ini bukan yang Anda inginkan:

  • Terkadang, lambat. Sinkronisasi dengan sistem eksternal tidak selalu instan, jadi Anda mungkin ingin melewatkannya kecuali jika diperlukan. Misalnya, Anda tidak ingin menyambung kembali ke server obrolan pada setiap penekanan papan ketik.
  • Terkadang, tidak benar. Misalnya, Anda tidak ingin memicu animasi fade-in komponen pada setiap penekanan papan ketik. Animasi seharusnya hanya diputar satu kali ketika komponen muncul untuk pertama kalinya.

Untuk mendemonstrasikan masalah ini, berikut adalah contoh sebelumnya dengan beberapa panggilan console.log dan input teks yang memperbarui state komponen induk. Perhatikan bagaimana pengetikan menyebabkan Effect dijalankan kembali:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Memanggil video.play()');
      ref.current.play();
    } else {
      console.log('Memanggil video.pause()');
      ref.current.pause();
    }
  });

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Jeda' : 'Putar'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Anda dapat memberi tahu React untuk melewatkan menjalankan ulang Effect yang tidak perlu dengan menspesifikasikan senarai dependencies sebagai argumen kedua pada pemanggilan useEffect. Mulai dengan menambahkan senarai kosong [] ke dalam contoh di atas pada baris 14:

useEffect(() => {
// ...
}, []);

Anda akan melihat error yang mengatakan React Hook useEffect has a missing dependency: 'isPlaying':

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Memanggil video.play()');
      ref.current.play();
    } else {
      console.log('Memanggil video.pause()');
      ref.current.pause();
    }
  }, []); // This causes an error

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Jeda' : 'Putar'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Masalahnya adalah kode di dalam Effect Anda tergantung pada prop isPlaying untuk memutuskan apa yang harus dilakukan, tetapi ketergantungan ini tidak dideklarasikan secara eksplisit. Untuk memperbaiki masalah ini, tambahkan isPlaying ke dalam senarai dependensi:

useEffect(() => {
if (isPlaying) { // Digunakan di sini...
// ...
} else {
// ...
}
}, [isPlaying]); // ...jadi harus dideklarasikan di sini!

Sekarang semua dependensi dideklarasikan, jadi tidak ada error. Menentukan [isPlaying] sebagai senarai dependensi memberi tahu React ia harus melewati menjalankan ulang Effect Anda apabila isPlaying sama seperti saat render sebelumnya. Dengan perubahan ini, mengetik pada input tidak menyebabkan Effect dijalankan ulang, tapi menekan Putar/Jeda akan menyebabkannya:

import { useState, useRef, useEffect } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      console.log('Memanggil video.play()');
      ref.current.play();
    } else {
      console.log('Memanggil video.pause()');
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

export default function App() {
  const [isPlaying, setIsPlaying] = useState(false);
  const [text, setText] = useState('');
  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Jeda' : 'Putar'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

Senarai dependensi dapat berisi lebih dari satu dependensi. React hanya akan melewatkan menjalankan ulang Effect jika semua dependensi yang Anda tentukan memiliki nilai yang sama persis dengan nilai yang mereka miliki saat render sebelumnya. React membandingkan nilai dependensi menggunakan fungsi pembanding Object.is. Lihat referensi useEffect untuk detailnya.

Perhatikan bahwa Anda tidak dapat “memilih” dependensi Anda. Anda akan mendapatkan lint error jika dependensi yang Anda tentukan tidak sesuai dengan apa yang diharapkan oleh React berdasarkan kode di dalam Effect Anda. Hal ini membantu menangkap banyak bug dalam kode Anda. Jika Anda tidak ingin beberapa kode dijalankan ulang, edit kode Effect itu sendiri untuk tidak “membutuhkan” dependensi tersebut.

Sandungan

Perilaku tanpa senarai dependensi dan dengan senarai dependensi kosong [] berbeda:

useEffect(() => {
// Ini dijalankan setiap render
});

useEffect(() => {
// Ini hanya dijalankan setiap pemasangan (ketika komponen ditampilkan)
}, []);

useEffect(() => {
// Ini dijalankan setiap pemasangan *dan juga* ketika a atau b telah berubah sejak render sebelumnya
}, [a, b]);

Kita akan mencermati secara dekat, apa arti “pemasangan” dalam langkah berikutnya.

Pendalaman

Mengapa ref dihilangkan dari senarai dependensi?

Effect ini menggunakan ref dan isPlaying, tapi hanya isPlaying yang dideklarasikan sebagai dependensi:

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);

Hal ini dikarenakan objek ref memiliki identitas yang stabil: React menjamin Anda akan selalu mendapatkan objek yang sama dari pemanggilan useRef yang sama pada setiap render. Objek tersebut tidak pernah berubah, sehingga tidak akan pernah dengan sendirinya menyebabkan Effect dijalankan ulang. Oleh karena itu, tidak masalah apakah Anda menyertakannya atau tidak. Memasukkannya juga tidak masalah:

function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying, ref]);

Fungsi set yang dikembalikan oleh useState juga memiliki identitas yang stabil, sehingga Anda akan sering melihat fungsi ini dihilangkan dari dependensi. Jika linter mengizinkan Anda menghilangkan sebuah dependensi tanpa kesalahan, maka hal ini aman untuk dilakukan.

Menghilangkan dependensi yang selalu stabil hanya berfungsi ketika linter dapat “melihat” bahwa objek tersebut stabil. Sebagai contoh, jika ref dioper dari komponen induk, Anda harus menspesifikasikannya dalam senarai dependensi. However, this is good because you can’t know whether the parent component always passes the same ref, or passes one of several refs conditionally. So your Effect would depend on which ref is passed.

Langkah 3: Tambahkan pembersihan jika diperlukan

Bayangkan contoh yang berbeda. Anda sedang menulis komponen ChatRoom yang perlu terhubung ke server obrolan ketika ditampilkan. Anda diberi API createConnection() yang mengembalikan sebuah objek dengan metode connect() dan disconnect(). Bagaimana Anda menjaga komponen tetap terhubung saat ditampilkan kepada pengguna?

Mulai dengan menulis logika Effect:

useEffect(() => {
const connection = createConnection();
connection.connect();
});

Akan sangat lambat untuk melakukan koneksi ke obrolan setelah setiap render ulang, jadi Anda menambahkan larik dependensi:

useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);

Kode di dalam Effect tidak menggunakan props atau state apapun, sehingga larik dependensi Anda adalah [] (kosong). Ini memberitahu React untuk hanya menjalankan kode ini ketika komponen “dipasang”, yaitu muncul di layar untuk pertama kalinya.

Mari kita coba menjalankan kode ini:

import { useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
  }, []);
  return <h1>Selamat datang di ruang obrolan!</h1>;
}

Effect ini hanya berjalan pada pemasangan, jadi Anda mungkin mengharapkan "✅ Menghubungkan..." dicetak sekali di konsol. Namun, jika Anda memeriksa konsol, "✅ Menghubungkan..." akan dicetak dua kali. Mengapa hal ini bisa terjadi?

Bayangkan komponen ChatRoom merupakan bagian dari aplikasi yang lebih besar dengan banyak layar yang berbeda. Pengguna memulai perjalanan mereka di halaman ChatRoom. Komponen dipasang dan memanggil connection.connect(). Kemudian bayangkan pengguna menavigasi ke layar lain—misalnya, ke halaman Pengaturan. Akhirnya, pengguna mengklik Kembali dan ChatRoom terpasang kembali. Hal ini akan membuat sambungan kedua—tetapi sambungan pertama tidak pernah diputuskan! Ketika pengguna menavigasi aplikasi, koneksi akan terus menumpuk.

Bug seperti ini mudah terlewatkan tanpa pengujian manual yang ekstensif. Untuk membantu Anda menemukannya dengan cepat, dalam pengembangan, React melakukan pemasangan ulang setiap komponen satu kali setelah pemasangan awal.

Melihat log "✅ Menghubungkan..." dua kali akan membantu Anda mengetahui masalah yang sebenarnya: kode Anda tidak menutup koneksi ketika komponen dilepas.

Untuk memperbaiki masalah ini, kembalikan fungsi cleanup dari Effect Anda:

useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);

React akan memanggil fungsi pembersihan Anda setiap kali sebelum Effect dijalankan kembali, dan satu kali lagi ketika komponen dilepas (dihapus). Mari kita lihat apa yang terjadi ketika fungsi pembersihan diimplementasikan:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Selamat datang di ruang obrolan!</h1>;
}

Sekarang Anda mendapatkan tiga log konsol dalam pengembangan:

  1. "✅ Menghubungkan..."
  2. "❌ Terputus."
  3. "✅ Menghubungkan..."

Ini adalah perilaku yang benar dalam pengembangan. Dengan memasang kembali komponen Anda, React memverifikasi bahwa navigasi menjauh dan kembali tidak akan merusak kode Anda. Memutuskan sambungan dan kemudian menyambungkannya kembali adalah hal yang seharusnya terjadi! Ketika Anda mengimplementasikan pembersihan dengan baik, seharusnya tidak ada perbedaan yang terlihat oleh pengguna antara menjalankan Effect sekali vs menjalankannya, membersihkannya, dan menjalankannya lagi. Ada pasangan panggilan tambahan untuk menghubungkan/memutuskan koneksi karena React sedang menyelidiki kode Anda untuk mencari bug dalam pengembangan. Ini adalah hal yang normal—jangan mencoba untuk menghilangkannya!

Dalam produksi, Anda hanya akan melihat "✅ Menghubungkan..." dicetak satu kali. Memasang kembali komponen hanya terjadi dalam pengembangan untuk membantu Anda menemukan Efek yang perlu dibersihkan. Anda dapat mematikan Strict Mode untuk keluar dari perilaku pengembangan, tetapi kami sarankan untuk tetap mengaktifkannya. Hal ini memungkinkan Anda menemukan banyak bug seperti di atas.

Bagaimana cara menangani Effect yang ditembakkan dua kali dalam pengembangan?

React secara sengaja memasang ulang komponen Anda dalam pengembangan untuk menemukan bug seperti pada contoh terakhir. Pertanyaan yang tepat bukanlah “bagaimana cara menjalankan sebuah Effect sekali saja”, tetapi “bagaimana cara memperbaiki Effect saya agar dapat berfungsi setelah dipasang ulang”.

Biasanya, jawabannya adalah menerapkan fungsi pembersihan. Fungsi pembersihan harus menghentikan atau membatalkan apa pun yang sedang dilakukan oleh Effect. Aturan praktisnya adalah bahwa pengguna seharusnya tidak dapat membedakan antara Effect yang berjalan sekali (seperti dalam produksi) dan urutan setup → cleanup → setup (seperti yang Anda lihat dalam pengembangan).

Sebagian besar Effect yang akan Anda tulis, akan sesuai dengan salah satu pola umum di bawah ini.

Mengontrol widget di luar React

Terkadang Anda perlu menambahkan widget UI yang tidak ditulis untuk React. Sebagai contoh, katakanlah Anda menambahkan komponen peta ke halaman Anda. Komponen ini memiliki metode setZoomLevel(), dan Anda ingin menjaga tingkat zoom tetap sinkron dengan variabel state zoomLevel dalam kode React Anda. Effect Anda akan terlihat seperti ini:

useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);

Perhatikan bahwa tidak ada pembersihan yang diperlukan dalam kasus ini. Dalam pengembangan, React akan memanggil Effect dua kali, tetapi ini tidak menjadi masalah karena memanggil setZoomLevel dua kali dengan nilai yang sama tidak akan melakukan apa-apa. Ini mungkin sedikit lebih lambat, tetapi ini tidak menjadi masalah karena tidak akan melakukan pemanggilan ulang yang tidak perlu dalam produksi.

Beberapa API mungkin tidak mengizinkan Anda memanggilnya dua kali berturut-turut. Misalnya, metode showModal dari elemen <dialog> bawaan akan melempar error jika Anda memanggilnya dua kali. Implementasi fungsi pembersihan untuk membuatnya menutup dialog:

useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);

Dalam pengembangan, Effect Anda akan memanggil showModal(), lalu segera close(), dan kemudian showModal() lagi. Ini memiliki perilaku yang terlihat oleh pengguna yang sama dengan memanggil showModal() satu kali, seperti yang akan Anda lihat dalam produksi.

Berlangganan events

Jika Efek Anda berlangganan sesuatu, fungsi pembersihan harus menghentikan langganan:

useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);

Dalam mode pengembangan, Effect Anda akan memanggil addEventListener(), lalu segera hapusEventListener(), dan kemudian addEventListener() lagi dengan event handler yang sama. Jadi hanya akan ada satu langganan yang aktif pada satu waktu. Ini memiliki perilaku yang terlihat oleh pengguna yang sama dengan memanggil addEventListener() sekali, seperti dalam produksi.

Memicu animasi

Jika Effect Anda menganimasikan sesuatu, fungsi pembersihan harus mengatur ulang animasi ke nilai awal:

useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Picu animasi
return () => {
node.style.opacity = 0; // Set ulang ke nilai awal
};
}, []);

Dalam mode pengembangan, opacity akan diatur ke 1, kemudian ke 0, dan kemudian ke 1 lagi. Ini seharusnya memiliki perilaku yang terlihat oleh pengguna yang sama dengan pengaturan ke 1 secara langsung, yang akan terjadi dalam produksi. Jika Anda menggunakan pustaka animasi pihak ketiga yang mendukung tweening, fungsi pembersihan Anda akan mengatur ulang timeline ke kondisi awal.

Mengambil data

Jika Efek Anda mengambil sesuatu, fungsi pembersihan harus membatalkan pengambilan atau mengabaikan hasilnya:

useEffect(() => {
let ignore = false;

async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}

startFetching();

return () => {
ignore = true;
};
}, [userId]);

Anda tidak dapat “membatalkan” network request yang telah terjadi, tetapi fungsi pembersihan Anda harus memastikan bahwa pengambilan data yang tidak relevan lagi tidak terus mempengaruhi aplikasi Anda. Jika userId berubah dari 'Alice' menjadi 'Bob', pembersihan memastikan bahwa respons 'Alice' diabaikan meskipun ia datang setelah 'Bob'.

Dalam mode pengembangan, Anda akan melihat dua fetch di tab Network. Tidak ada yang salah dengan hal itu. Dengan pendekatan di atas, Effect pertama akan segera dibersihkan sehingga salinan variabel ignore akan disetel ke true. Jadi, meskipun ada request tambahan, hal itu tidak akan mempengaruhi state berkat pemeriksaan if (!ignore).

Dalam mode produksi, hanya akan ada satu request. Jika request kedua dalam pengembangan mengganggu Anda, pendekatan terbaik adalah menggunakan solusi yang menduplikasi request dan menyimpan responsnya di antara komponen:

function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...

Hal ini tidak hanya akan meningkatkan pengalaman pengembangan, tetapi juga membuat aplikasi Anda terasa lebih cepat. Sebagai contoh, pengguna yang menekan tombol Kembali tidak perlu menunggu data dimuat lagi, karena data tersebut akan di-cache. Anda bisa membuat cache sendiri atau menggunakan salah satu dari banyak alternatif untuk fetching secara manual di Effects.

Pendalaman

Apa saja alternatif yang bagus untuk pengambilan data di Effects?

Menulis panggilan fetch di dalam Effects adalah cara populer untuk mengambil data, terutama di aplikasi yang sepenuhnya berbasis klien. Namun, ini adalah pendekatan yang sangat manual dan memiliki kelemahan yang signifikan:

  • Effects tidak berjalan di server. Ini berarti bahwa HTML awal yang di-render di server hanya akan menyertakan status pemuatan tanpa data. Komputer klien harus mengunduh semua JavaScript dan me-render aplikasi Anda hanya untuk mengetahui bahwa sekarang ia perlu memuat data. Hal ini sangat tidak efisien.
  • Mengambil data secara langsung dalam Effects memudahkan untuk menciptakan “air terjun (waterfall) jaringan”. Anda me-render komponen induk, mengambil beberapa data, me-render komponen anak, dan kemudian komponen anak mulai mengambil datanya. Jika jaringan tidak terlalu cepat, hal ini jauh lebih lambat daripada mengambil semua data secara paralel.
  • Mengambil data secara langsung dalam Effects biasanya berarti Anda tidak melakukan pramuat atau cache data. Sebagai contoh, jika komponen dilepas dan kemudian dipasang lagi, komponen tersebut harus mengambil data lagi.
  • Sangat tidak ergonomis. Ada cukup banyak kode boilerplate yang terlibat ketika menulis panggilan fetch dengan cara yang tidak mengalami bug seperti race condition.

Daftar kelemahan ini tidak spesifik untuk React. Ini berlaku untuk mengambil data saat pemasangan komponen dengan pustaka apa pun. Seperti halnya dengan routing, pengambilan data bukanlah hal yang sepele untuk dilakukan dengan baik, jadi kami merekomendasikan pendekatan berikut ini:

  • Jika Anda menggunakan kerangka kerja (framework), gunakan mekanisme pengambilan data yang sudah ada di dalamnya. Kerangka kerja React modern memiliki mekanisme pengambilan data terintegrasi yang efisien dan tidak mengalami kendala di atas.
  • Jika tidak, pertimbangkan untuk menggunakan atau membangun cache sisi klien. Solusi sumber terbuka (open source) yang populer termasuk React Query, useSWR, dan React Router 6.4+. Anda juga dapat membuat solusi sendiri, dalam hal ini Anda dapat menggunakan Effects di dalamnya, tetapi menambahkan logika untuk menduplikasi request, menyimpan respons dalam cache, dan menghindari waterfall jaringan (dengan melakukan pramuat data atau mengangkat kebutuhan data ke route).

Anda dapat terus mengambil data secara langsung di Effects jika tidak ada satu pun dari pendekatan ini yang cocok untuk Anda.

Mengirim analitik

Perhatikan kode berikut ini yang mengirimkan event analitik pada kunjungan halaman:

useEffect(() => {
logVisit(url); // Sends a POST request
}, [url]);

Dalam mode pengembangan, logVisit akan dipanggil dua kali untuk setiap URL, sehingga Anda mungkin tergoda untuk mencoba memperbaikinya. Kami sarankan untuk membiarkan kode ini apa adanya. Seperti contoh-contoh sebelumnya, tidak ada perbedaan perilaku yang dilihat oleh pengguna antara menjalankannya sekali dan menjalankannya dua kali. Dari sudut pandang praktis, logVisit tidak boleh melakukan apa pun dalam pengembangan karena Anda tidak ingin log dari mesin pengembangan mempengaruhi metrik produksi. Komponen Anda akan dimuat ulang setiap kali Anda menyimpan berkasnyanya, sehingga komponen tersebut tetap mencatat kunjungan ekstra dalam pengembangan.

Dalam mode produksi, tidak akan ada log kunjungan yang terduplikasi.

Untuk men-debug event analitik yang Anda kirimkan, Anda bisa men-deploy aplikasi Anda ke lingkungan staging (yang berjalan dalam mode produksi) atau untuk sementara tidak menggunakan Strict Mode dan pengecekan ulang khusus pengembangannya. Anda juga dapat mengirimkan analitik dari event handler perubahan route, bukan dari Effects. Untuk analitik yang lebih tepat, intersection observers dapat membantu melacak komponen mana yang ada di tampilan layar dan berapa lama komponen tersebut tetap terlihat.

Not an Effect: Initializing the application

Some logic should only run once when the application starts. You can put it outside your components:

if (typeof window !== 'undefined') { // Check if we're running in the browser.
checkAuthToken();
loadDataFromLocalStorage();
}

function App() {
// ...
}

This guarantees that such logic only runs once after the browser loads the page.

Not an Effect: Buying a product

Sometimes, even if you write a cleanup function, there’s no way to prevent user-visible consequences of running the Effect twice. For example, maybe your Effect sends a POST request like buying a product:

useEffect(() => {
// 🔴 Wrong: This Effect fires twice in development, exposing a problem in the code.
fetch('/api/buy', { method: 'POST' });
}, []);

You wouldn’t want to buy the product twice. However, this is also why you shouldn’t put this logic in an Effect. What if the user goes to another page and then presses Back? Your Effect would run again. You don’t want to buy the product when the user visits a page; you want to buy it when the user clicks the Buy button.

Buying is not caused by rendering; it’s caused by a specific interaction. It should run only when the user presses the button. Delete the Effect and move your /api/buy request into the Buy button event handler:

function handleClick() {
// ✅ Buying is an event because it is caused by a particular interaction.
fetch('/api/buy', { method: 'POST' });
}

This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From a user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, then pressing Back to view the page again. React verifies that your components abide by this principle by remounting them once in development.

Putting it all together

This playground can help you “get a feel” for how Effects work in practice.

This example uses setTimeout to schedule a console log with the input text to appear three seconds after the Effect runs. The cleanup function cancels the pending timeout. Start by pressing “Mount the component”:

import { useState, useEffect } from 'react';

function Playground() {
  const [text, setText] = useState('a');

  useEffect(() => {
    function onTimeout() {
      console.log('⏰ ' + text);
    }

    console.log('🔵 Schedule "' + text + '" log');
    const timeoutId = setTimeout(onTimeout, 3000);

    return () => {
      console.log('🟡 Cancel "' + text + '" log');
      clearTimeout(timeoutId);
    };
  }, [text]);

  return (
    <>
      <label>
        What to log:{' '}
        <input
          value={text}
          onChange={e => setText(e.target.value)}
        />
      </label>
      <h1>{text}</h1>
    </>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Unmount' : 'Mount'} the component
      </button>
      {show && <hr />}
      {show && <Playground />}
    </>
  );
}

You will see three logs at first: Schedule "a" log, Cancel "a" log, and Schedule "a" log again. Three second later there will also be a log saying a. As you learned earlier, the extra schedule/cancel pair is because React remounts the component once in development to verify that you’ve implemented cleanup well.

Now edit the input to say abc. If you do it fast enough, you’ll see Schedule "ab" log immediately followed by Cancel "ab" log and Schedule "abc" log. React always cleans up the previous render’s Effect before the next render’s Effect. This is why even if you type into the input fast, there is at most one timeout scheduled at a time. Edit the input a few times and watch the console to get a feel for how Effects get cleaned up.

Type something into the input and then immediately press “Unmount the component”. Notice how unmounting cleans up the last render’s Effect. Here, it clears the last timeout before it has a chance to fire.

Finally, edit the component above and comment out the cleanup function so that the timeouts don’t get cancelled. Try typing abcde fast. What do you expect to happen in three seconds? Will console.log(text) inside the timeout print the latest text and produce five abcde logs? Give it a try to check your intuition!

Three seconds later, you should see a sequence of logs (a, ab, abc, abcd, and abcde) rather than five abcde logs. Each Effect “captures” the text value from its corresponding render. It doesn’t matter that the text state changed: an Effect from the render with text = 'ab' will always see 'ab'. In other words, Effects from each render are isolated from each other. If you’re curious how this works, you can read about closures.

Pendalaman

Each render has its own Effects

You can think of useEffect as “attaching” a piece of behavior to the render output. Consider this Effect:

export default function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);

return <h1>Welcome to {roomId}!</h1>;
}

Let’s see what exactly happens as the user navigates around the app.

Initial render

The user visits <ChatRoom roomId="general" />. Let’s mentally substitute roomId with 'general':

// JSX for the first render (roomId = "general")
return <h1>Welcome to general!</h1>;

The Effect is also a part of the rendering output. The first render’s Effect becomes:

// Effect for the first render (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the first render (roomId = "general")
['general']

React runs this Effect, which connects to the 'general' chat room.

Re-render with same dependencies

Let’s say <ChatRoom roomId="general" /> re-renders. The JSX output is the same:

// JSX for the second render (roomId = "general")
return <h1>Welcome to general!</h1>;

React sees that the rendering output has not changed, so it doesn’t update the DOM.

The Effect from the second render looks like this:

// Effect for the second render (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the second render (roomId = "general")
['general']

React compares ['general'] from the second render with ['general'] from the first render. Because all dependencies are the same, React ignores the Effect from the second render. It never gets called.

Re-render with different dependencies

Then, the user visits <ChatRoom roomId="travel" />. This time, the component returns different JSX:

// JSX for the third render (roomId = "travel")
return <h1>Welcome to travel!</h1>;

React updates the DOM to change "Welcome to general" into "Welcome to travel".

The Effect from the third render looks like this:

// Effect for the third render (roomId = "travel")
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
},
// Dependencies for the third render (roomId = "travel")
['travel']

React compares ['travel'] from the third render with ['general'] from the second render. One dependency is different: Object.is('travel', 'general') is false. The Effect can’t be skipped.

Before React can apply the Effect from the third render, it needs to clean up the last Effect that did run. The second render’s Effect was skipped, so React needs to clean up the first render’s Effect. If you scroll up to the first render, you’ll see that its cleanup calls disconnect() on the connection that was created with createConnection('general'). This disconnects the app from the 'general' chat room.

After that, React runs the third render’s Effect. It connects to the 'travel' chat room.

Unmount

Finally, let’s say the user navigates away, and the ChatRoom component unmounts. React runs the last Effect’s cleanup function. The last Effect was from the third render. The third render’s cleanup destroys the createConnection('travel') connection. So the app disconnects from the 'travel' room.

Development-only behaviors

When Strict Mode is on, React remounts every component once after mount (state and DOM are preserved). This helps you find Effects that need cleanup and exposes bugs like race conditions early. Additionally, React will remount the Effects whenever you save a file in development. Both of these behaviors are development-only.

Rekap

  • Unlike events, Effects are caused by rendering itself rather than a particular interaction.
  • Effects let you synchronize a component with some external system (third-party API, network, etc).
  • By default, Effects run after every render (including the initial one).
  • React will skip the Effect if all of its dependencies have the same values as during the last render.
  • You can’t “choose” your dependencies. They are determined by the code inside the Effect.
  • Empty dependency array ([]) corresponds to the component “mounting”, i.e. being added to the screen.
  • In Strict Mode, React mounts components twice (in development only!) to stress-test your Effects.
  • If your Effect breaks because of remounting, you need to implement a cleanup function.
  • React will call your cleanup function before the Effect runs next time, and during the unmount.

Tantangan 1 dari 4:
Focus a field on mount

In this example, the form renders a <MyInput /> component.

Use the input’s focus() method to make MyInput automatically focus when it appears on the screen. There is already a commented out implementation, but it doesn’t quite work. Figure out why it doesn’t work, and fix it. (If you’re familiar with the autoFocus attribute, pretend that it does not exist: we are reimplementing the same functionality from scratch.)

import { useEffect, useRef } from 'react';

export default function MyInput({ value, onChange }) {
  const ref = useRef(null);

  // TODO: This doesn't quite work. Fix it.
  // ref.current.focus()    

  return (
    <input
      ref={ref}
      value={value}
      onChange={onChange}
    />
  );
}

To verify that your solution works, press “Show form” and verify that the input receives focus (becomes highlighted and the cursor is placed inside). Press “Hide form” and “Show form” again. Verify the input is highlighted again.

MyInput should only focus on mount rather than after every render. To verify that the behavior is right, press “Show form” and then repeatedly press the “Make it uppercase” checkbox. Clicking the checkbox should not focus the input above it.