Menghilangkan Effect dependensi

Saat anda menulis sebuah Effect, linter akan memverifikasi bahwa anda telah memasukan 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.

You will learn

  • Cara memperbaiki Effect tak terbatas perulangan dependensi?
  • Apa yang harus dilakukan bila anda ingin menghapus dependensi?
  • Cara membaca nilai dari Effect anda tanpa “bereaksi” dengannya?
  • Bagaimana dan mengapa menghindari objek dan fungsi dependensi?
  • Mengapa menekan linter dependensi berbahaya, dan alih-alih apa yang harus 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('umum'); return ( <> <label> Pilih runag 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 dideklrasikan
// ...
}

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 ole 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 variable dan fungsi dideklrasaikan 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 = 'musik'; // 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 = 'musik'; 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.

Pitfall

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.

Deep Dive

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 Effect Event.)

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 memasukan 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:

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

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.

Are you reading some state to calculate the next state?

This Effect updates the messages state variable with a newly created array every time a new message arrives:

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

It uses the messages variable to create a new array starting with all the existing messages and adds the new message at the end. However, since messages is a reactive value read by an Effect, it must be a dependency:

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]); // ✅ All dependencies declared
// ...

And making messages a dependency introduces a problem.

Every time you receive a message, setMessages() causes the component to re-render with a new messages array that includes the received message. However, since this Effect now depends on messages, this will also re-synchronize the Effect. So every new message will make the chat re-connect. The user would not like that!

To fix the issue, don’t read messages inside the Effect. Instead, pass an updater function to 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]); // ✅ All dependencies declared
// ...

Notice how your Effect does not read the messages variable at all now. You only need to pass an updater function like msgs => [...msgs, receivedMessage]. React puts your updater function in a queue and will provide the msgs argument to it during the next render. This is why the Effect itself doesn’t need to depend on messages anymore. As a result of this fix, receiving a chat message will no longer make the chat re-connect.

Do you want to read a value without “reacting” to its changes?

Under Construction

This section describes an experimental API that has not yet been released in a stable version of React.

Suppose that you want to play a sound when the user receives a new message unless isMuted is 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();
}
});
// ...

Since your Effect now uses isMuted in its code, you have to add it to the dependencies:

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]); // ✅ All dependencies declared
// ...

The problem is that every time isMuted changes (for example, when the user presses the “Muted” toggle), the Effect will re-synchronize, and reconnect to the chat. This is not the desired user experience! (In this example, even disabling the linter would not work—if you do that, isMuted would get “stuck” with its old value.)

To solve this problem, you need to extract the logic that shouldn’t be reactive out of the Effect. You don’t want this Effect to “react” to the changes in isMuted. Move this non-reactive piece of logic into an Effect Event:

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]); // ✅ All dependencies declared
// ...

Effect Events let you split an Effect into reactive parts (which should “react” to reactive values like roomId and their changes) and non-reactive parts (which only read their latest values, like onMessage reads isMuted). Now that you read isMuted inside an Effect Event, it doesn’t need to be a dependency of your Effect. As a result, the chat won’t re-connect when you toggle the “Muted” setting on and off, solving the original issue!

Wrapping an event handler from the props

You might run into a similar problem when your component receives an event handler as a 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]); // ✅ All dependencies declared
// ...

Suppose that the parent component passes a different onReceiveMessage function on every render:

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

Since onReceiveMessage is a dependency, it would cause the Effect to re-synchronize after every parent re-render. This would make it re-connect to the chat. To solve this, wrap the call in an Effect Event:

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]); // ✅ All dependencies declared
// ...

Effect Events aren’t reactive, so you don’t need to specify them as dependencies. As a result, the chat will no longer re-connect even if the parent component passes a function that’s different on every re-render.

Separating reactive and non-reactive code

In this example, you want to log a visit every time roomId changes. You want to include the current notificationCount with every log, but you don’t want a change to notificationCount to trigger a log event.

The solution is again to split out the non-reactive code into an Effect Event:

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

useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}

You want your logic to be reactive with regards to roomId, so you read roomId inside of your Effect. However, you don’t want a change to notificationCount to log an extra visit, so you read notificationCount inside of the Effect Event. Learn more about reading the latest props and state from Effects using Effect Events.

Does some reactive value change unintentionally?

Sometimes, you do want your Effect to “react” to a certain value, but that value changes more often than you’d like—and might not reflect any actual change from the user’s perspective. For example, let’s say that you create an options object in the body of your component, and then read that object from inside of your Effect:

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

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

This object is declared in the component body, so it’s a reactive value. When you read a reactive value like this inside an Effect, you declare it as a dependency. This ensures your Effect “reacts” to its changes:

// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...

It is important to declare it as a dependency! This ensures, for example, that if the roomId changes, your Effect will re-connect to the chat with the new options. However, there is also a problem with the code above. To see it, try typing into the input in the sandbox below, and watch what happens in the console:

import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); // Temporarily disable the linter to demonstrate the problem // 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>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }

In the sandbox above, the input only updates the message state variable. From the user’s perspective, this should not affect the chat connection. However, every time you update the message, your component re-renders. When your component re-renders, the code inside of it runs again from scratch.

A new options object is created from scratch on every re-render of the ChatRoom component. React sees that the options object is a different object from the options object created during the last render. This is why it re-synchronizes your Effect (which depends on options), and the chat re-connects as you type.

This problem only affects objects and functions. In JavaScript, each newly created object and function is considered distinct from all the others. It doesn’t matter that the contents inside of them may be the same!

// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

Object and function dependencies can make your Effect re-synchronize more often than you need.

This is why, whenever possible, you should try to avoid objects and functions as your Effect’s dependencies. Instead, try moving them outside the component, inside the Effect, or extracting primitive values out of them.

Move static objects and functions outside your component

If the object does not depend on any props and state, you can move that object outside your component:

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

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

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...

This way, you prove to the linter that it’s not reactive. It can’t change as a result of a re-render, so it doesn’t need to be a dependency. Now re-rendering ChatRoom won’t cause your Effect to re-synchronize.

This works for functions too:

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();
}, []); // ✅ All dependencies declared
// ...

Since createOptions is declared outside your component, it’s not a reactive value. This is why it doesn’t need to be specified in your Effect’s dependencies, and why it won’t ever cause your Effect to re-synchronize.

Move dynamic objects and functions inside your Effect

If your object depends on some reactive value that may change as a result of a re-render, like a roomId prop, you can’t pull it outside your component. You can, however, move its creation inside of your Effect’s code:

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]); // ✅ All dependencies declared
// ...

Now that options is declared inside of your Effect, it is no longer a dependency of your Effect. Instead, the only reactive value used by your Effect is roomId. Since roomId is not an object or function, you can be sure that it won’t be unintentionally different. In JavaScript, numbers and strings are compared by their content:

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

Thanks to this fix, the chat no longer re-connects if you edit the input:

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>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <hr /> <ChatRoom roomId={roomId} /> </> ); }

However, it does re-connect when you change the roomId dropdown, as you would expect.

This works for functions, too:

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]); // ✅ All dependencies declared
// ...

You can write your own functions to group pieces of logic inside your Effect. As long as you also declare them inside your Effect, they’re not reactive values, and so they don’t need to be dependencies of your Effect.

Read primitive values from objects

Sometimes, you may receive an object from props:

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

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...

The risk here is that the parent component will create the object during rendering:

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

This would cause your Effect to re-connect every time the parent component re-renders. To fix this, read information from the object outside the Effect, and avoid having object and function dependencies:

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]); // ✅ All dependencies declared
// ...

The logic gets a little repetitive (you read some values from an object outside an Effect, and then create an object with the same values inside the Effect). But it makes it very explicit what information your Effect actually depends on. If an object is re-created unintentionally by the parent component, the chat would not re-connect. However, if options.roomId or options.serverUrl really are different, the chat would re-connect.

Calculate primitive values from functions

The same approach can work for functions. For example, suppose the parent component passes a function:

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

To avoid making it a dependency (and causing it to re-connect on re-renders), call it outside the Effect. This gives you the roomId and serverUrl values that aren’t objects, and that you can read from inside your 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]); // ✅ All dependencies declared
// ...

This only works for pure functions because they are safe to call during rendering. If your function is an event handler, but you don’t want its changes to re-synchronize your Effect, wrap it into an Effect Event instead.

Recap

  • Dependencies should always match the code.
  • When you’re not happy with your dependencies, what you need to edit is the code.
  • Suppressing the linter leads to very confusing bugs, and you should always avoid it.
  • To remove a dependency, you need to “prove” to the linter that it’s not necessary.
  • If some code should run in response to a specific interaction, move that code to an event handler.
  • If different parts of your Effect should re-run for different reasons, split it into several Effects.
  • If you want to update some state based on the previous state, pass an updater function.
  • If you want to read the latest value without “reacting” it, extract an Effect Event from your Effect.
  • In JavaScript, objects and functions are considered different if they were created at different times.
  • Try to avoid object and function dependencies. Move them outside the component or inside the Effect.

Challenge 1 of 4:
Fix a resetting interval

This Effect sets up an interval that ticks every second. You’ve noticed something strange happening: it seems like the interval gets destroyed and re-created every time it ticks. Fix the code so that the interval doesn’t get constantly re-created.

import { useState, useEffect } from 'react'; export default function Timer() { const [count, setCount] = useState(0); useEffect(() => { console.log('✅ Creating an interval'); const id = setInterval(() => { console.log('⏰ Interval tick'); setCount(count + 1); }, 1000); return () => { console.log('❌ Clearing an interval'); clearInterval(id); }; }, [count]); return <h1>Counter: {count}</h1> }