Menghilangkan Efek dependensi
Saat Anda menulis sebuah Efek, linter akan memverifikasi bahwa Anda telah memasukkan setiap nilai reaktif (seperti props dan state) yang dibaca Efek dalam daftar dependensi Efek. Ini memastikan bahwa Efek Anda tetap tersinkronisasi dengan props dan state terbaru dari komponen Anda. Dependensi yang tidak perlu dapat menyebabkan Efek Anda berjalan terlalu sering, atau bahkan membuat perulangan tak terbatas. Ikuti panduan ini untuk meninjau dan menghapus dependensi yang tidak perlu dari Efek Anda.
You will learn
- Bagaimana untuk memperbaiki perulangan dependensi Efek tak terbatas (Inifinite Effect)?
- Apa yang harus dilakukan ketika Anda ingin menghapus dependensi?
- Bagaimana untuk membaca nilai dari Efek 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 Efek, pertama Anda menentukan cara memulai dan menghentikan apa pun yang Anda ingin dari Efek 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 Efek 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
// ...
}
Efek “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 Efek 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 Efek Anda. Setiap nilai reaktif yang digunakan oleh kode Efek 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); // Efek ini membaca nilai reaktif tersebut
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Jadi Anda harus menentukan nilai reaktif tersebut sebagai dependesi dari Efek 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. Efek 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:
- Pertama, Anda mengubah kode kode Efek Anda atau bagaimana nilai reaktif Anda dideklarasikan.
- Kemudian, Anda mengikuti linter dan menyesuaikan dependensi agar sesuai dengan kode yang Anda ubah.
- 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 Efek 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 Efek! Untungnya, ada daftar dari cara umum yang bisa Anda coba di bawah ini.
Deep Dive
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 Efek “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 Efek 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 Efek.)
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 Efek untuk merefleksikan kode, lihat pada daftar dependensi. Apakah masuk akal jika Efek 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 Efek 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 Efek 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 Efek yang “bereaksi” terhadap submitted
yang bernilai true
:
function Form() {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
// 🔴 Hindari: Logika Event-specific di dalam Efek
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 Efek
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, Efek akan berjalan kembali, sehingga akan menampilkan notifikasi yang sama lagi!
Masalahnya di sini adalah ini seharusnya tidak menjadi Efek 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 Efek dan cara menghapus Efrk yang tidak perlu.
Apakah Efek Anda melakukan beberapa hal yang tidak terkait?
Pertanyaan berikutnya yang harus Anda tanyakan pada diri sendiri adalah apakah Efek 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 Efek. 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 Efek 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 Efek 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 Efek 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, Efek 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:
- Anda ingin menyinkronkan state
cities
ke jaringan berdasarkan propcountry
. - Anda ingin menyinkronkan state
areas
ke jaringan berdasarkan propcity
.
Membagi logika menjadi dua Efek, 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, Efek pertama hanya akan berjalan kembali jika country
berubah, sedangkan Efek kedua akan berjalan kembali jika city
berubah. Anda telah memisahkannya dengan tujuan: dua hal yang berbeda disinkronkan oleh dua Efek yang terpisah. Dua Efek 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 Efek ini masih benar. Setiap Efek harus mewakili proses sinkronisasi independen. Dalam contoh ini, menghapus satu Efek tidak merusak logika Efek 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?
Efek 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 Efek, 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 Efek ini bergantung pada messages
, ini juga akan menyinkronkan ulang Efek. Jadi setiap pesan baru akan membuat obrolan terhubung kembali. Pengguna tidak akan menyukai hal itu!
Untuk memperbaiki masalah ini, jangan membaca messages
di dalam Efek. 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 Efek 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 Efek 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?
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 Efek 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”), Efek 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 Efek. Anda tidak ingin Efek ini “bereaksi” terhadap perubahan dari isMuted
. Pindahkan logika non-reaktif ini ke dalam Event Efek:
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 Efek memungkinkan Anda membagi Efek 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 Efek, ia tidak perlu menjadi dependensi Efek 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 Efek 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 Efek:
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 Efek 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 Efek:
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 Efek Anda. Nmaun, Anda tidak ingin perubahan pada notificationCount
untuk mencatat kunjungan tambahan, jadi Anda membaca notificationCount
di dalam Event Efek. Pelajari lebih lanjut tentang membaca props dan state dari Efek menggunakan Event Efek.
Apakah beberapa nilai reaktif berubah secara tidak sengaja
Terkadang, Anda ingin Efek 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 Efek 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 Efek, Anda mendeklarasikannya sebagai dependensi. Hal ini memastikan Efek 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, Efek 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 Efek 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 Efek Anda melakukan sinkronisasi ulang lebih sering daripada yang Anda perlukan.
Inilah sebabnya mengapa, jika memungkinkan, Anda harus mencoba menghindari objek dan fungsi sebagai dependensi Efek Anda. Sebagai gantinya, cobalah memindahkannya di luar komponen, di dalam Efek, 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 Efek 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();
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 Efek Anda, dan mengapa ia tidak akan menyebabkan Efek Anda melakukan sinkronisasi ulang.
Memindahkan objek dan fungsi dinamis di dalam Efek 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 Efek 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 Efek Anda, tidak lagi menjadi dependensi dari Efek Anda. Sebaliknya, satu-satunya nilai reaktif yang digunakan oleh Efek 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 Efek Anda. Selama Anda juga mendeklarasikannya di dalam Efek Anda, mereka bukan nilai reaktif, sehingga tidak perlu menjadi dependensi dari Efek 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 Efek Anda terhubung kembali setiap kali komponen induk di-render ulang. Untuk mengatasinya, baca informasi dari objek di luar Efek, 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 Efek, dan kemudian membuat objek dengan nilai yang sama di dalam Efek). Tetapi hal ini membuatnya sangat eksplisit tentang informasi apa yang sebenarnya bergantung pada Efek 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 Efek. Ini akan memberi Anda nilai roomId
dan serverUrl
yang bukan objek, dan Anda dapat membacanya dari dalam Efek:
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 Efek Anda, bungkuslah menjadi sebuah Event Efek.
Recap
- 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 Efek Anda harus dijalankan ulang karena alasan yang berbeda, pisahkan menjadi beberapa Efek.
- Jika Anda ingin memperbarui beberapa state berdasarkan state sebelumnya, berikan fungsi updater.
- Jika Anda ingin membaca nilai terbaru tanpa “bereaksi”, ekstrak Event Efek dari Efek 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 Efek.
Challenge 1 of 4: Memperbaiki interval pengaturan ulang
Efek 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> }