Menghilangkan dependensi Effect

Saat Anda menulis sebuah Effect, linter akan memverifikasi bahwa Anda telah memasukkan setiap nilai reaktif (seperti props dan state) yang dibaca Effect dalam daftar dependensi Effect. Ini memastikan bahwa Effect Anda tetap tersinkronisasi dengan props dan state terbaru dari komponen Anda. Dependensi yang tidak perlu dapat menyebabkan Effect Anda berjalan terlalu sering, atau bahkan membuat perulangan tak terbatas. Ikuti panduan ini untuk meninjau dan menghapus dependensi yang tidak perlu dari Effect Anda.

Anda akan mempelajari

  • Bagaimana untuk memperbaiki perulangan dependensi Effect tak terbatas (Inifinite Effect)?
  • Apa yang harus dilakukan ketika Anda ingin menghapus dependensi?
  • Bagaimana untuk membaca nilai dari Effect Anda tanpa “bereaksi” kepadanya?
  • Bagaimana dan mengapa menghindari objek dan fungsi dependensi?
  • Mengapa menekan dependensi linter itu berbahaya, dan apa yang seharusnya dilakukan?

Dependensi harus sesuai dengan kode

Saat Anda menulis sebuah Effect, pertama Anda menentukan cara memulai dan menghentikan apa pun yang Anda ingin dari Effect Anda lakukan:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// ...
}

Kemudian, jika Anda membiarkan dependensi Effect kosong ([]), linter akan menyarankan dependensi yang tepat:

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

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Perbaiki kesalahan disini!
  return <h1>Selamat datang di ruang {roomId}!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Pilih ruang obrolan:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">umum</option>
          <option value="travel">travel</option>
          <option value="music">musik</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Isi sesuai dengan apa yang linter katakan:

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Semua dependensi dideklarasikan
// ...
}

Effect “bereaksi” terhadap nilai reaktif. Karena roomId adalah nilai reaktif (dapat berubah karena render ulang), linter memverifikasi bahwa Anda telah menetapkannya sebagai sebuah dependensi. JIka roomId menerima nilai yang berbeda, React akan menyinkronkan ulang Effect Anda. Ini memastikan obrolan tetap terhubung ke ruang yang dipilih dan “bereaksi” dengan dropdown:

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

const serverUrl = 'https://localhost:1234';

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

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Pilih ruang obrolan:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">umum</option>
          <option value="travel">travel</option>
          <option value="music">musik</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Untuk menghapus dependensi, pastikan bahwa itu bukan dependensi

Perhatikan bahwa Anda tidak dapat “memilih” dependensi dari Effect Anda. Setiap nilai reaktif yang digunakan oleh kode Effect Anda harus dideklarasikan dalam daftar dependensi. Daftar dependensi ditentukan oleh kode disekitarnya:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) { // Ini adalah nilai reaktif
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Effect ini membaca nilai reaktif tersebut
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Jadi Anda harus menentukan nilai reaktif tersebut sebagai dependesi dari Effect Anda
// ...
}

Nilai reaktif termasuk props dan semua variabel dan fungsi dideklarasikan langsung di dalam komponen Anda. Ketika roomId adalah nilai reaktif, Anda tidak dapat menghapusnya dari daftar dependensi. Linter tidak akan mengizinkannya:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect memiliki dependensi yang hilang: 'roomId'
// ...
}

Dan linter akan benar! Ketika roomId mungkin berubah dari waktu ke waktu, ini akan menimbulkan bug dalam kode Anda.

Untuk menghapus dependensi, “buktikan” kepada linter bahwa itu tidak perlu menjadi sebuah dependensi. Misalnya, Anda dapat mengeluarkan roomId dari komponen untuk membuktikan bahwa ia tidak reaktif dan tidak akan berubah saat render ulang:

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Bukan nilai reaktif lagi

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Semua dependensi dideklarasikan
// ...
}

Sekarang roomId bukan nilai reaktif (dan tidak berubah dalam render ulang), ia tidak perlu menjadi sebuah dependensi:

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

const serverUrl = 'https://localhost:1234';
const roomId = 'music';

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

Inilah mengapa Anda sekarang dapat menentukan ([]) daftar dependensi kosong. Effect Anda benar-benar tidak bergantung pada nilai reaktif lagi, jadi itu benar-benar tidak dijalankan ulang ketika salah satu props atau state komponen berubah.

Untuk mengubah dependensi, ubah kodenya

Anda mungkin memperhatikan pola dalam alur kerja Anda:

  1. Pertama, Anda mengubah kode kode Effect Anda atau bagaimana nilai reaktif Anda dideklarasikan.
  2. Kemudian, Anda mengikuti linter dan menyesuaikan dependensi agar sesuai dengan kode yang Anda ubah.
  3. Jika kamu tidak puas dengan daftar dependensi, Anda kembali ke langkah pertama (dan mengubah kodenya kembali).

Bagian terakhir ini penting. Jika Anda ingin mengubah dependensi, ubah kode sekitarnya lebih dulu. Anda bisa menganggap daftar dependensi sebagai sebuah daftar dari semua niali reaktif yang digunakan oleh kode Effect Anda. Anda tidak memilih apa yang dimasukan ke dalam daftar tersebut. Daftar mendeskripsikan kode Anda. Untuk mengubah daftar dependensi, ubah kodenya.

Ini mungkin terasa seperti menyelesaikan persamaan. Anda mungkin memulai dengan tujuan (misalnya, untuk menghapus dependensi), dan Anda perlu “menemukan” kode yang sesuai dengan tujuan tersebut. Tidak semua orang menganggap memecahkan persamaan itu menyenangkan, dan hal yang sama bisa dikatakan tentang menulis Effect! Untungnya, ada daftar dari cara umum yang bisa Anda coba di bawah ini.

Sandungan

Jika Anda memiliki basis kode yang sudah ada, Anda mungkin memiliki beberapa Effect yang menekan linter seperti ini:

useEffect(() => {
// ...
// 🔴 Hindari menekan linter seperti ini:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

Ketika dependensi tidak sesuai dengan kode, ada risiko yang sangat tinggi memunculkan bug Dengan menekan linter, Anda “bohong” kepada React tentang nilai yang bergantung pada Effect Anda.

Sebagai gantinya, gunakan teknik di bawah ini.

Pendalaman

Mengapa menekan linter dependensi sangat berbahaya?

Menekan linter menyebabkan bug yang sangat tidak intuitif yang sulit ditemukan dan diperbaiki. Berikut salah satu contohnya:

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Pencacah: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Setiap detik, kenaikan sebesar:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

Katakanlah Anda ingin menjalankan Effect “hanya saat mount”. Anda telah membaca ([]) dependensi kosong melakukannya, jadi Anda memutuskan untuk mengabaikan linter, dan dengan paksa menentukan [] sebagai dependensi.

Pencacah ini seharusnya bertambah setiap detik dengan jumlah yang dapat dikonfigurasi dengan 2 tombol. Namun, karena Anda “berbohong” kepada React bahwa Effect ini tidak bergantung pada apa pun, React selamanya akan tetap menggunakan fungsi onTick dari render awal. Selama render tersebut, count adalah 0 and increment adalah 1. Inilah mengapa onTick dari render tersebut selalu memanggil setCount(0 + 1) setiap, dan Anda selalu melihat 1. Bug seperti ini sulit untuk diperbaiki ketika tersebar dibeberapa komponen.

Selalu ada solusi yang lebih baik daripada mengabaikan linter! Untuk memperbaiki kode ini, Anda perlu menambahkan onTick ke dalam daftar dependensi. (Untuk memastikan interval hanya disetel sekali, buat onTick sebagai Event Effect.)

Sebaiknya perlakukan eror lint dependensi sebagai eror kompilasi. Jika Anda tidak menekannya, Anda tidak akan pernah melihat eror seperti ini. Sisa dari halaman ini mendokumentasikan untuk kasus ini dan kasus lainnya.

Menghapus dependensi yang tidak perlu

Setiap kali Anda mengatur dependensi Effect untuk merefleksikan kode, lihat pada daftar dependensi. Apakah masuk akal jika Effect dijalankan ulang ketika salah satu dependensi ini berubah? Terkadang, jawabannya adalah “tidak”:

  • Anda mungkin ingin menjalankan kembali bagian yang berbeda dalam kondisi yang berbeda.
  • Anda mungkin ingin hanya membaca nilai terbaru dari beberapa dependensi alih-alih “bereaksi” terhadap perubahannya.
  • Sebuah dependensi dapat berubah terlalu sering secara tidak sengaja karena merupakan objek atau fungsi.

Untuk menemukan solusi yang tepat, Anda harus menjawab beberapa pertanyaan tentang Effect Anda. Mari kita telusuri pertanyaan-pertanyaan tersebut.

Haruskah kode ini dipindahkan ke event handler?

Hal pertama yang harus Anda pikirkan adalah apakah kode ini harus menjadi Effect atau tidak.

Bayangkan sebuah formlir. Ketika dikirim, Anda mengatur variabel state submitted menjadi true. Anda perlu mengirim permintaan POST dan menampilkan notifikasi. Anda telah memasukkan logika ini ke dalam Effect yang “bereaksi” terhadap submitted yang bernilai true:

function Form() {
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
if (submitted) {
// 🔴 Hindari: Logika Event-specific di dalam Effect
post('/api/register');
showNotification('Berhasil mendaftar!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Kemudian, Anda ingin menyesuaikan pesan notifikasi sesuai dengan tema saat ini, sehingga Anda membaca tema saat ini. Ketika theme dideklarasikan di badan komponen, tema merupakan nilai reaktif, jadi Anda menambahkannya sebagai dependensi:

function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);

useEffect(() => {
if (submitted) {
// 🔴 Hindari: Logika Event-specific di dalam Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ Semua dependensi dideklarasikan

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Dengan melakukan hal ini, Anda telah memunculkan bug. Bayangkan Anda mengirimkan formulir terlebih dahulu kemudian beralih antara tema Gelap dan Terang. theme akan berubah, Effect akan berjalan kembali, sehingga akan menampilkan notifikasi yang sama lagi!

Masalahnya di sini adalah ini seharusnya tidak menjadi Effect sejak awal. Anda ingin mengririm permintaan POST tersebut dan menampilkan notifikasi sebagai respon atas pengiriman formulir, yang merupakan interaksi tertentu. Untuk menjalankan beberapa kode sebagai respon terhadap interaksi tertentu, letakkan logika tersebut langsung ke dalam event handler yang sesuai:

function Form() {
const theme = useContext(ThemeContext);

function handleSubmit() {
// ✅ Baik: Logika Event-specific dipanggil dari event handler
post('/api/register');
showNotification('Berhasil mendaftar!', theme);
}

// ...
}

Sekarang kode tersebut berada di dalam event handler, kode tersebut tidak reaktif—jadi itu hanya akan berjalan saat pengguna mengirimkan formulir. Baca slebih lanjut tentang memilih antara event handlers dan Effect dan cara menghapus Efrk yang tidak perlu.

Apakah Effect Anda melakukan beberapa hal yang tidak terkait?

Pertanyaan berikutnya yang harus Anda tanyakan pada diri sendiri adalah apakah Effect Anda melakukan beberapa hal yang tidak berhubungan.

Bayangkan Anda membuat formulir pengiriman di mana pengguna perlu memilih kota dan wilayah mereka. Anda mengambil daftar cities dari server sesuai dengan country yang dipilih untuk menampilkannya dalam menu dropdown:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ Semua dependensi dideklarasikan

// ...

Ini adalah contoh yang baik untuk mengambil data dari Effect. Anda menyinkronkan state cities dengan jaringan sesuai dengan props country. Anda tidak dapat melakukan hal ini di dalam event handler karena Anda harus mengambil data segera setelah ShippingForm ditampilkan dan setiap kali country berubah (tidak peduli interaksi mana yang menyebabkannya).

Sekarang katakanlah Anda menambahkan kotak pilihan kedua untuk area kota, yang akan mengambil areas untuk city yang sedang dipilih. Anda dapat memulai dengan menambahkan panggilan fetch kedua untuk daftar area di dalam Effect yang sama:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Hindari: Satu Effect menyinkronkan dua proses independen
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ Semua dependensi dideklarasikan

// ...

Namun, karena Effect sekarang menggunakan variabel state city, nda harus menambahkan city ke dalam daftar dependensi. Hal ini, pada akhirnya, menimbulkan masalah: ketika pengguna memilih kota yang berbeda, Effect akan menjalankan ulang dan memanggil fetchCities(country). Akibatnya, Anda akan mengambil ulang daftar kota berkali-kali.

Masalah dengan kode ini adalah Anda menyinkronkan dua hal berbeda yang tidak berhubungan:

  1. Anda ingin menyinkronkan state cities ke jaringan berdasarkan prop country.
  2. Anda ingin menyinkronkan state areas ke jaringan berdasarkan prop city.

Membagi logika menjadi dua Effect, yang masing-masing bereaksi terhadap prop yang perlu disinkronkan:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ Semua dependensi dideklarasikan

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ Semua dependensi dideklarasikan
// ...

Sekarang, Effect pertama hanya akan berjalan kembali jika country berubah, sedangkan Effect kedua akan berjalan kembali jika city berubah. Anda telah memisahkannya dengan tujuan: dua hal yang berbeda disinkronkan oleh dua Effect yang terpisah. Dua Effect yang terpisah memiliki dua daftar dependensi yang terpisah, jadi keduanya tidak akan memicu satu sama lain secara tidak sengaja.

Kode akhir lebih panjang dari aslinya, tetapi pemisahan Effect ini masih benar. Setiap Effect harus mewakili proses sinkronisasi independen. Dalam contoh ini, menghapus satu Effect tidak merusak logika Effect lainnya. Ini berarti mereka menyinkronkan hal-hal yang berbeda, dan akan lebih baik jika dipisahkan. Jika Anda khawatir tentang duplikasi, Anda dapat mengembangkan kode ini dengan mengekstrak logika berulang ke dalam Hook khusus.

Apakah Anda membaca beberapa state untuk menghitung state berikutnya?

Effect ini memperbarui variabel state messages dengan senarai yang baru dibuat setiap kali ada pesan baru yang masuk:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...

Ini menggunakan variabel messages untuk membuat senarai baru yang dimulai dengan semua pesan yang ada dan menambahkan pesan baru di bagian akhir. Namun, karena messages adalah nilai reaktif yang dibaca oleh Effect, maka itu harus menjadi sebuah dependensi:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ Semua dependensi dideklarasikan
// ...

Dan menjadikan messages sebagai dependensi akan menimbulkan masalah.

Setiap kali Anda menerima pesan, setMessages() menyebabkan komponen di-render ulang dengan senarai messages baru yang memuat pesan yang diterima. Namun, karena Effect ini bergantung pada messages, ini juga akan menyinkronkan ulang Effect. Jadi setiap pesan baru akan membuat obrolan terhubung kembali. Pengguna tidak akan menyukai hal itu!

Untuk memperbaiki masalah ini, jangan membaca messages di dalam Effect. Sebagai gantinya, berikan fungsi updater untuk setMessages:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ Semua dependensi dideklarasikan
// ...

Perhatikan bagaimana Effect Anda tidak membaca variabel messages sama sekali sekarang. Anda hanya perlu mengoper fungsi updater seperti msgs => [...msgs, receivedMessage]. React menaruh fungsi updater Anda dalam antrian dan akan memberikan argumen msgs kepada fungsi tersebut pada saat render berikutnya. Inilah sebabnya mengapa Effect sendiri tidak perlu bergantung pada messages lagi. Dengan adanya perbaikan ini, menerima pesan obrolan tidak lagi membuat obrolan tersambung kembali.

Apakah Anda ingin membaca nilai tanpa “bereaksi” terhadap perubahannya?

Dalam Pengembangan

Bagian ini menjelaskan API experimental yang belum dirilis dalam versi stabil React.

Misalkan Anda ingin memainkan bunyi saat pengguna menerima pesan baru kecuali isMuted bernilai true:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...

Karena Effect Anda sekarang menggunakan isMuted dalam kodenya, Anda harus menambahkannya dalam dependensi:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ Semua dependensi dideklarasikan
// ...

Masalahnya adalah setiap kali isMuted berubah (misalnya, saat pengguna menekan tombol “Muted”), Effect dakan menyinkronkan ulang, dan menghubungkan kembali ke obrloan. Ini bukan user experience yang diinginkan! (Dalam contoh ini, bahkan menonaktifkan linter pun tidak akan berhasil—jika Anda melakukannya, isMuted akan “terjebak” dengan nilai sebelumnya.)

Untuk mengatasi masalah ini, Anda perlu mengekstrak logika yang seharusnya tidak reaktif dari Effect. Anda tidak ingin Effect ini “bereaksi” terhadap perubahan dari isMuted. Pindahkan logika non-reaktif ini ke dalam Event Effect:

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

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ Semua dependensi dideklarasikan
// ...

Event Effect memungkinkan Anda membagi Effect menjadi bagian reaktif (yang seharusnya “bereaksi” terhadap nilai reaktif seperti roomId dan perubahannya) dan bagian non-reaktif (yang hanya membaca nilai terbarunya, seperti onMessage membaca isMuted). Sekarang setelah Anda membaca isMuted di dalam Event Effect, ia tidak perlu menjadi dependensi Effect Anda. Hasilnya, obrolan tidak akan tehubung kembali saat Anda men-toggle pengaturan “Dibisukan”, dan menyelesaikan masalah aslinya!

Membungkus event handler dari props

Anda mungkin mengalami masalah yang sama ketika komponen Anda menerima event handler sebagai prop:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ Semua dependensi dideklarasikan
// ...

Misalkan komponen induk meneruskan fungsi onReceiveMessage yang berbeda pada setiap render:

<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>

Karena onReceiveMessage adalah sebuah dependensi, ini akan menyebabkan Effect untuk menyinkronkan ulang setelah setiap induk dirender ulang. Hal ini akan membuat terhubung kembali ke obrolan. Untuk mengatasi ini, bungkus panggilan tersebut dalam sebuah Event Effect:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ Semua dependensi dideklarasikan
// ...

Event Effect tidak reaktif, jadi Anda tidak perlu menetapkannya sebagai dependensi. Hasilnya, obrolan tidak akan terhubung kembali meskipun komponen induk meneruskan fungsi yang berbeda pada setiap render ulang.

Memisahkan kode reaktif dan non-reaktif

Dalam contoh ini, Anda ingin mencatat kunjungan setiap kali roomId berubah. Anda ingin memasukkan notificationCount saat ini dengan setiap log, namun Anda tidak ingin perubahan notificationCount memicu log event.

Solusinya adalah sekali lagi membagi kode non-reaktif menjadi Event Effect:

function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});

useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ Semua dependensi dideklarasikan
// ...
}

Anda ingin logika Anda menjadi reaktif terhadap roomId, sehingga Anda membaca roomId di dalam Effect Anda. Nmaun, Anda tidak ingin perubahan pada notificationCount untuk mencatat kunjungan tambahan, jadi Anda membaca notificationCount di dalam Event Effect. Pelajari lebih lanjut tentang membaca props dan state dari Effect menggunakan Event Effect.

Apakah beberapa nilai reaktif berubah secara tidak sengaja

Terkadang, Anda ingin Effect Anda “beraksi” terhadap nilai tertentu, tetapi nilai tersebut berubah lebih sering daripada yang Anda inginkan—dan mungkin tidak mencerminkan perubahan yang sebenarnya dari sudut pandang pengguna. For example, Sebagai contoh, katakanlah Anda membuat objek options dalam badan komponen Anda, dan kemudian membaca objek tersebut dari dalam Effect Anda:

function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};

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

Objek ini dideklarasikan di dalam badan komponen, jadi ini adalah nilai reaktif. Ketika Anda membaca nilai reaktif seperti ini di dalam Effect, Anda mendeklarasikannya sebagai dependensi. Hal ini memastikan Effect Anda “bereaksi” terhadap perubahannya:

// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Semua dependensi dideklarasikan
// ...

Penting untuk mendeklarasikannya sebagai dependensi, Hal ini memastikan, misalnya, jika roomId berubah, Effect Anda akan terhubung kembali dengan options baru. Namun, ada juga masalah dengan kode di atas. Untuk melihatnya, coba ketik masukan di sandbox di bawah ini, dan lihat apa yang terjadi di konsol:

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

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  // Nonaktifkan linter untuk sementara guna mendemonstrasikan masalahnya
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

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

  return (
    <>
      <h1>Selamat datang di ruang {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Pilih ruang obrolan:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">umum</option>
          <option value="travel">travel</option>
          <option value="music">musik</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Pada sandbox di atas, masukan hanya akan memperbarui variabel state message. Dari sudut pandang pengguna, hal ini seharusnya tidak mempengaruhi koneksi obrolan. Namun, setiap kali Anda memperbarui message, komponen Anda akan di-render ulang. Saat kompenen Anda di-render ulang, kode di dalamnya akan berjalan lagi dari awal.

Objek options baru dibuat dari awal pada setiap render ulang komponen ChatRoom. React melihat bahwa objek options adalah objek yang berbeda dari objek options yang dibuat pada render terakhir. Inilah mengapa React melakukan sinkronisasi ulang pada Effect Anda (yang bergantung pada options), dan obrolan akan tersambung kembali setelah Anda mengetik.

Masalah ini hanya mempengaruhi objek dan fungsi. Dalam JavaScript, setiap objek dan fungsi yang baru dibuat dianggap berbeda dari yang lainnya. Tidak masalah jika isi di dalamnya mungkin sama!

// Selama render pertama
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// Selama render berikutnya
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// Ini adalah dua objek yang berbeda!
console.log(Object.is(options1, options2)); // false

Dependensi objek dan fungsi dapat membuat Effect Anda melakukan sinkronisasi ulang lebih sering daripada yang Anda perlukan.

Inilah sebabnya mengapa, jika memungkinkan, Anda harus mencoba menghindari objek dan fungsi sebagai dependensi Effect Anda. Sebagai gantinya, cobalah memindahkannya di luar komponen, di dalam Effect, atau mengekstrak nilai primitif dari komponen tersebut.

Memindahkan objek dan fungsi statis di luar komponen Anda

Jika objek tidak bergantung pada props dan state apa pun, Anda dapat memindahkan objek tersebut di luar komponen Anda:

const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Semua dependensi dideklarasikan
// ...

Dengan cara ini, Anda membuktikan kepada linter bahwa itu tidak reaktif. Ini tidak dapat berubah sebagai hasil dari render ulang, jadi tidak perlu menjadi dependensi. Sekarang me-render ulang ChatRoom tidak akan menyebabkan Effect Anda melakukan sinkronisasi ulang.

Ini juga berlaku untuk fungsi:

function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ Semua dependensi dideklrasikan
// ...

Karena createOptions dideklarasikan di luar komponen Anda, ini bukan nilai reaktif. Inilah sebabnya mengapa ia tidak perlu ditentukan dalam dependensi Effect Anda, dan mengapa ia tidak akan menyebabkan Effect Anda melakukan sinkronisasi ulang.

Memindahkan objek dan fungsi dinamis di dalam Effect Anda

Jika objek Anda bergantung pada beberapa nilai reaktif yang dapat berubah sebagai hasil dari render ulang, seperti prop roomId, Anda tidak dapat menariknya ke luar komponen Anda. Namun, Anda dapat memindahkan pembuatannya di dalam kode Effect Anda:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Semua dependensi dideklarasikan
// ...

Sekarang options dideklarasikan di dalam Effect Anda, tidak lagi menjadi dependensi dari Effect Anda. Sebaliknya, satu-satunya nilai reaktif yang digunakan oleh Effect Anda adalah roomId. Karena roomId bukan objek atau fungsi, Anda dapat yakin bahwa itu tidak akan berbeda secara tidak sengaja. Dalam JavaScript, numbers dan string dibandingkan berdasarkan isinya:

// Selama render pertama
const roomId1 = 'music';

// Selama render berikutnya
const roomId2 = 'music';

// Kedua string ini sama!
console.log(Object.is(roomId1, roomId2)); // true

Berkat perbaikan ini, obrolan tidak lagi terhubung kembali jika Anda mengedit masukan:

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

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

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

  return (
    <>
      <h1>Selamat datang di ruang {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Pilih ruang obrolan:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">umum</option>
          <option value="travel">travel</option>
          <option value="music">musik</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Namun, ini akan terhubung kembali ketika Anda mengubah dropdown roomId, seperti yang Anda harapkan.

Hal ini juga berlaku untuk fungsi-fungsi lainnya:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}

const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Semua dependensi dideklarasikan
// ...

Anda dapat menulis fungsi Anda sendiri untuk mengelompokkan bagian logika di dalam Effect Anda. Selama Anda juga mendeklarasikannya di dalam Effect Anda, mereka bukan nilai reaktif, sehingga tidak perlu menjadi dependensi dari Effect Anda.

Membaca nilai primitif dari objek

Terkadang, Anda mungkin menerima objek dari props:

function ChatRoom({ options }) {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Semua dependensi dideklarasikan
// ...

Risikonya di sini adalah komponen induk akan membuat objek selama rendering:

<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>

Hal ini akan menyebabkan Effect Anda terhubung kembali setiap kali komponen induk di-render ulang. Untuk mengatasinya, baca informasi dari objek di luar Effect, dan hindari dependensi objek dan fungsi:

function ChatRoom({ options }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Semua dependensi dideklarasikan
// ...

Logikanya menjadi sedikit berulang (Anda membaca beberapa nilai dari objek di luar Effect, dan kemudian membuat objek dengan nilai yang sama di dalam Effect). Tetapi hal ini membuatnya sangat eksplisit tentang informasi apa yang sebenarnya bergantung pada Effect Anda. Jika sebuah objek dibuat ulang secara tidak sengaja oleh komponen induk, obrolan tidak akan tersambung kembali. Namun, jika options.roomId atau options.serverUrl benar-benar berbeda, obrolan akan tersambung kembali.

Menghitung nilai primitif dari fungsi

Pendekatan yang sama dapat digunakan untuk fungsi. Sebagai contoh, misalkan komponen induk meneruskan sebuah fungsi:

<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>

Untuk menghindari menjadikannya dependensi (dan menyebabkannya terhubung kembali pada render ulang), panggil di luar Effect. Ini akan memberi Anda nilai roomId dan serverUrl yang bukan objek, dan Anda dapat membacanya dari dalam Effect:

function ChatRoom({ getOptions }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = getOptions();
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Semua dependensi dideklarasikan
// ...

Ini hanya berfungsi untuk fungsi murni karena aman untuk dipanggil selama rendering. Jika fungsi Anda adalah sebuah event handler, tetapi Anda tidak ingin perubahannya menyinkronkan ulang Effect Anda, bungkuslah menjadi sebuah Event Effect.

Rekap

  • Dependensi harus selalu sesuai dengan kodenya.
  • Ketika Anda tidak puas dengan dependensi Anda, yang perlu Anda edit adalah kodenya.
  • Menekan linter akan menyebabkan bug yang sangat membingungkan, dan Anda harus selalu menghindarinya.
  • Untuk menghapus sebuah dependensi, Anda perlu “membuktikan” kepada linter bahwa dependensi tersebut tidak diperlukan.
  • Jika beberapa kode harus berjalan sebagai respons terhadap interaksi tertentu, pindahkan kode tersebut ke event handler.
  • Jika beberapa bagian dari Effect Anda harus dijalankan ulang karena alasan yang berbeda, pisahkan menjadi beberapa Effect.
  • Jika Anda ingin memperbarui beberapa state berdasarkan state sebelumnya, berikan fungsi updater.
  • Jika Anda ingin membaca nilai terbaru tanpa “bereaksi”, ekstrak Event Effect dari Effect Anda.
  • Dalam JavaScript, objek dan fungsi dianggap berbeda jika dibuat pada waktu yang berbeda.
  • Cobalah untuk menghindari ketergantungan objek dan fungsi. Pindahkan mereka di luar komponen atau di dalam Effect.

Tantangan 1 dari 4:
Memperbaiki interval pengaturan ulang

Effect ini menetapkan interval yang berdetak setiap detik. Anda telah memperhatikan sesuatu yang aneh terjadi: sepertinya interval tersebut dihancurkan dan dibuat ulang setiap kali berdetak. Perbaiki kode sehingga interval tidak terus-menerus dibuat ulang.

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('✅ Membuat interval');
    const id = setInterval(() => {
      console.log('⏰ Ketukan interval');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Membersihkan interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Pencacah: {count}</h1>
}