useEffect
useEffect
adalah Hook React yang memungkinkan Anda menyinkronkan komponen dengan sistem eksternal.
useEffect(setup, dependencies?)
- Referensi
- Penggunaan
- Menghubungkan ke sistem eksternal
- Membungkus Effect dalam Hook kustom
- Mengontrol widget non-React
- Mengambil data dengan Effects
- Menentukan dependensi yang responsif
- Memperbarui state berdasarkan state sebelumnya dari sebuah Effect
- Menghapus dependensi objek yang tidak diperlukan
- Menghapus dependensi fungsi yang tidak perlu
- Membaca props dan state terbaru dari sebuah Effect
- Menampilkan konten yang berbeda di server dan klien
- Penyelesaian masalah
- Effect Anda berjalan dua kali saat komponen terpasang
- Effect Anda berjalan setelah setiap render ulang
- Effect Anda terus berjalan dalam siklus tak terbatas
- Logika cleanup Anda berjalan meskipun komponen Anda tidak dilepas
- Effect Anda melakukan sesuatu yang visual, dan Anda melihat kedipan sebelum berjalan
Referensi
useEffect(setup, dependencies?)
Panggil useEffect
di level atas komponen Anda untuk mendeklarasikan sebuah Effect:
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Lihat lebih banyak contoh di bawah ini.
Parameter
-
setup
: Fungsi dengan logika Effect Anda. Fungsi setup Anda juga dapat secara opsional mengembalikan fungsi cleanup. Ketika komponen Anda pertama kali ditambahkan ke DOM, React akan menjalankan fungsi setup Anda. Setelah setiap re-render dengan dependensi yang berubah, React akan pertama-tama menjalankan fungsi cleanup (jika Anda menyediakannya) dengan nilai lama, dan kemudian menjalankan fungsi setup Anda dengan nilai baru. Setelah komponen Anda dihapus dari DOM, React akan menjalankan fungsi cleanup Anda untuk terakhir kalinya. -
dependensi
opsional: Daftar semua nilai reaktif yang direferensikan di dalam kodesetup
. Nilai reaktif meliputi props, state, dan semua variabel dan fungsi yang dideklarasikan langsung di dalam body komponen Anda. Jika linter Anda dikonfigurasi untuk React, itu akan memverifikasi bahwa setiap nilai reaktif dijelaskan dengan benar sebagai dependensi. Daftar dependensi harus memiliki jumlah item yang konstan dan ditulis secara inline seperti[dep1, dep2, dep3]
. React akan membandingkan setiap dependensi dengan nilai sebelumnya menggunakan perbandinganObject.is
. Jika Anda mengabaikan argumen ini, Effect Anda akan berjalan ulang setelah setiap re-render dari komponen. Lihat perbedaan antara melewatkan array dependensi, array kosong, dan tidak ada dependensi sama sekali.
Kembalian
useEffect
mengembalikan undefined
.
Catatan Penting
-
useEffect
adalah Hook, sehingga Anda hanya dapat memanggilnya di level atas komponen Anda atau Hook Anda sendiri. Anda tidak dapat memanggilnya di dalam loop atau kondisi. Jika Anda membutuhkannya, ekstraklah komponen baru dan pindahkan state ke dalamnya. -
Jika Anda tidak mencoba untuk menyinkronkan dengan sistem eksternal tertentu, kemungkinan Anda tidak memerlukan sebuah Effect.
-
Ketika Strict Mode diaktifkan, React akan menjalankan satu siklus setup+cleanup tambahan hanya untuk pengembangan sebelum setup sebenarnya yang pertama. Ini adalah stress-test yang memastikan bahwa logika cleanup Anda “mencerminkan” logika setup Anda dan menghentikan atau membatalkan apa pun yang dilakukan setup. Jika ini menyebabkan masalah, implementasikan fungsi cleanup.
-
Jika beberapa dependensi Anda adalah objek atau fungsi yang didefinisikan di dalam komponen, ada risiko bahwa mereka akan membuat Effect berjalan ulang lebih sering dari yang diperlukan. Untuk mengatasinya, hapus dependensi objek dan fungsi yang tidak diperlukan. Anda juga dapat mengekstrak pembaruan state dan logika non-reaktif di luar Effect Anda.
-
Jika Effect Anda tidak disebabkan oleh interaksi (seperti klik), React dapat menjalankan Effect Anda sebelum browser menampilkan layar yang diperbarui. Ini memastikan bahwa hasil Effect dapat diamati oleh sistem event. Biasanya, ini berfungsi seperti yang diharapkan. Namun, jika Anda harus menunda pekerjaan hingga setelah penggambaran, seperti
alert()
, Anda dapat menggunakansetTimeout
. Lihat reactwg/react-18/128 untuk informasi lebih lanjut. -
Meskipun Effect Anda disebabkan oleh interaksi (seperti klik), React dapat mengizinkan browser untuk menggambar ulang layar sebelum memproses pembaruan state di dalam Efek Anda. Biasanya, ini berfungsi seperti yang diharapkan. Namun, jika Anda harus memblokir browser agar tidak menggambar ulang layar, Anda perlu mengganti
useEffect
denganuseLayoutEffect
. -
Effect hanya berjalan di sisi klien. Ia tidak akan berjalan selama rendering di server.
Penggunaan
Menghubungkan ke sistem eksternal
Beberapa komponen perlu terhubung dengan jaringan, beberapa API browser, atau perpustakaan pihak ketiga, saat ditampilkan pada halaman. Sistem-sistem ini tidak dikendalikan oleh React, sehingga disebut sebagai sistem eksternal.
Untuk menghubungkan komponen Anda ke sistem eksternal, panggil useEffect
pada level atas komponen Anda:
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Anda perlu melewatkan dua argumen ke useEffect
:
- Sebuah fungsi setup dengan kode setup yang menghubungkan ke sistem tersebut.
- Fungsi tersebut harus mengembalikan sebuah fungsi cleanup dengan kode cleanup yang memutus koneksi dari sistem tersebut.
- Sebuah daftar dependensi termasuk setiap nilai dari komponen Anda yang digunakan di dalam fungsi-fungsi tersebut.
React memanggil fungsi setup dan cleanup Anda kapan saja diperlukan, yang mungkin terjadi beberapa kali:
- Kode setup Anda dijalankan ketika komponen Anda ditambahkan ke halaman (mounts).
- Setelah setiap re-render dari komponen Anda di mana dependensi telah berubah:
- Pertama, kode cleanup Anda dijalankan dengan props dan state yang lama.
- Kemudian, kode setup Anda dijalankan dengan props dan state yang baru.
- Kode cleanup Anda dijalankan satu kali terakhir setelah komponen Anda dihapus dari halaman (unmounts).
Mari ilustrasikan urutan ini untuk contoh di atas.
Ketika komponen ChatRoom
di atas ditambahkan ke halaman, itu akan terhubung ke ruang obrolan dengan serverUrl
dan roomId
awal. Jika salah satu dari serverUrl
atau roomId
berubah sebagai hasil dari re-render (misalnya, jika pengguna memilih ruang obrolan yang berbeda dalam dropdown), Effect Anda akan memutuskan koneksi dari ruang sebelumnya, dan terhubung ke yang berikutnya. Ketika komponen ChatRoom
dihapus dari halaman, Effect Anda akan memutuskan koneksi satu kali terakhir.
Untuk membantu Anda menemukan bug, dalam pengembangan React menjalankan setup dan cleanup satu kali ekstra sebelum setup. Ini adalah pengujian stress-test yang memverifikasi logika Effect Anda diimplementasikan dengan benar. Jika ini menyebabkan masalah yang terlihat, fungsi cleanup Anda kekurangan beberapa logika. Fungsi cleanup harus menghentikan atau membatalkan apa yang dilakukan oleh fungsi setup. Aturan praktisnya adalah bahwa pengguna tidak boleh dapat membedakan antara setup yang dipanggil sekali (seperti di produksi) dan urutan setup → cleanup → setup (seperti di pengembangan). Lihat solusi umum.
Cobalah untuk menulis setiap Effect sebagai proses independen dan berpikir tentang satu siklus setup/cleanup pada suatu waktu. Tidak harus masalah apakah komponen Anda sedang mounting, updating, atau unmounting. Ketika logika cleanup Anda “mencerminkan” logika setup dengan benar, Effect Anda tangguh terhadap menjalankan setup dan cleanup sesering yang diperlukan.
Contoh 1 dari 5: Menghubungkan ke server obrolan
Pada contoh ini, komponen ChatRoom
menggunakan sebuah Effect untuk tetap terhubung ke sistem eksternal yang didefinisikan dalam chat.js
. Tekan “Buka obrolan” untuk membuat komponen ChatRoom
muncul. Sandbox ini berjalan dalam mode pengembangan, sehingga terdapat siklus koneksi-dan-putus tambahan, seperti yang dijelaskan di sini. Cobalah mengubah roomId
dan serverUrl
menggunakan dropdown
dan input, dan lihat bagaimana Effect terhubung kembali ke obrolan. Tekan “Tutup obrolan” untuk melihat Effect memutuskan koneksi untuk terakhir kalinya.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [roomId, serverUrl]); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); 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> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Membungkus Effect dalam Hook kustom
Effects adalah “pintu darurat”: Anda menggunakannya ketika Anda perlu “keluar dari React” dan ketika tidak ada solusi bawaan yang lebih baik untuk kasus penggunaan Anda. Jika Anda sering perlu menulis Effects secara manual, itu biasanya menjadi tanda bahwa Anda perlu mengekstrak beberapa custom Hooks untuk perilaku umum yang dibutuhkan komponen Anda.
Misalnya, custom Hook useChatRoom
ini “menyembunyikan” logika Effect Anda di balik API yang lebih deklaratif:
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
Lalu kamu bisa menggunakannya dari komponen mana pun seperti ini:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
Ada juga banyak custom Hooks yang sangat bagus untuk setiap tujuan yang tersedia di ekosistem React.
Pelajari lebih lanjut tentang cara mengelompokkan Effects ke dalam Custom Hooks.
Contoh 1 dari 3: Custom useChatRoom
Hook
Contoh ini sama dengan salah satu contoh sebelumnya, tetapi logikanya diekstraksi ke dalam custom Hook.
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl }); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); 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> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Mengontrol widget non-React
Terkadang, Anda ingin menjaga sistem eksternal tetap disinkronkan dengan beberapa prop atau state dari komponen React Anda.
Misalnya, jika Anda memiliki widget peta pihak ketiga atau komponen pemutar video yang ditulis tanpa React, Anda dapat menggunakan Effect untuk memanggil metode pada widget tersebut yang membuat state sesuai dengan state saat ini dari komponen React Anda. Effect ini menciptakan sebuah instance dari kelas MapWidget
yang didefinisikan dalam map-widget.js
. Ketika Anda mengubah prop zoomLevel
dari komponen Map
, Effect memanggil setZoom()
pada instance kelas untuk menjaganya tetap disinkronkan:
import { useRef, useEffect } from 'react'; import { MapWidget } from './map-widget.js'; export default function Map({ zoomLevel }) { const containerRef = useRef(null); const mapRef = useRef(null); useEffect(() => { if (mapRef.current === null) { mapRef.current = new MapWidget(containerRef.current); } const map = mapRef.current; map.setZoom(zoomLevel); }, [zoomLevel]); return ( <div style={{ width: 200, height: 200 }} ref={containerRef} /> ); }
Pada contoh ini, sebuah fungsi cleanup tidak diperlukan karena kelas MapWidget
hanya mengelola node DOM yang diberikan kepadanya. Setelah komponen Map
React dihapus dari pohon(tree), baik node DOM maupun instance kelas MapWidget
akan otomatis dihapus oleh mesin JavaScript pada browser.
Mengambil data dengan Effects
Anda dapat menggunakan sebuah Effect untuk mengambil data untuk komponen Anda. Perlu diingat bahwa jika Anda menggunakan sebuah framework, menggunakan mekanisme pengambilan data dari framework Anda akan jauh lebih efisien daripada menulis Effects secara manual.
Jika Anda ingin mengambil data dari sebuah Effect secara manual, kode Anda mungkin akan terlihat seperti ini:
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if (!ignore) {
setBio(result);
}
});
return () => {
ignore = true;
};
}, [person]);
// ...
Perhatikan variabel ignore
yang diinisialisasi dengan nilai false
dan diatur menjadi true
selama cleanup. Ini memastikan kode Anda tidak mengalami “race conditions”: respon jaringan dapat tiba dalam urutan yang berbeda dari yang Anda kirimkan.
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { let ignore = false; setBio(null); fetchBio(person).then(result => { if (!ignore) { setBio(result); } }); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ?? 'Loading...'}</i></p> </> ); }
Anda juga dapat menulis kode dengan menggunakan sintaksis async
/ await
, namun Anda masih perlu menyediakan fungsi cleanup:
import { useState, useEffect } from 'react'; import { fetchBio } from './api.js'; export default function Page() { const [person, setPerson] = useState('Alice'); const [bio, setBio] = useState(null); useEffect(() => { async function startFetching() { setBio(null); const result = await fetchBio(person); if (!ignore) { setBio(result); } } let ignore = false; startFetching(); return () => { ignore = true; } }, [person]); return ( <> <select value={person} onChange={e => { setPerson(e.target.value); }}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> <option value="Taylor">Taylor</option> </select> <hr /> <p><i>{bio ?? 'Loading...'}</i></p> </> ); }
Menulis pengambilan data langsung di Effects menjadi repetitif dan sulit untuk menambahkan optimasi seperti caching dan server rendering nanti. Lebih mudah untuk menggunakan Custom Hook - baik yang Anda buat sendiri atau yang dipelihara oleh komunitas.
Pendalaman
Menulis panggilan fetch
di dalam Effects adalah cara yang populer untuk mengambil data, terutama dalam aplikasi yang sepenuhnya berbasis klien. Namun, ini adalah pendekatan yang sangat manual dan memiliki beberapa kekurangan:
- Effects tidak berjalan di server. Ini berarti HTML yang dirender oleh server hanya akan berisi state loading tanpa data. Komputer klien harus mengunduh semua JavaScript dan merender aplikasi Anda hanya untuk menemukan bahwa sekarang ia perlu memuat data. Ini tidak efisien.
- Mengambil data secara langsung dalam Effects membuatnya mudah untuk membuat “waterfalls jaringan”. Anda merender komponen induk(parent), ia mengambil beberapa data, merender komponen anak(child), dan kemudian mereka mulai mengambil data mereka. Jika jaringannya tidak terlalu cepat, ini jauh lebih lambat daripada mengambil semua data secara paralel.
- Mengambil data secara langsung dalam Effects biasanya berarti Anda tidak memuat atau menyimpan data di cache. Misalnya, jika komponen di-unmount dan kemudian di-mount lagi, maka ia harus mengambil data lagi.
- Tidak ergonomis. Ada cukup banyak kode boilerplate yang terlibat saat menulis panggilan
fetch
dengan cara yang tidak menderita dari bug seperti race condition.
Daftar kekurangan ini tidak spesifik untuk React. Ini berlaku untuk mengambil data saat mount dengan library manapun. Seperti dengan routing, pengambilan data tidak mudah dilakukan dengan baik, jadi kami sarankan pendekatan berikut:
- Jika Anda menggunakan framework, gunakan mekanisme pengambilan data bawaannya. Framework React modern memiliki mekanisme pengambilan data terintegrasi yang efisien dan tidak menderita dari masalah di atas.
- Jika tidak, pertimbangkan untuk menggunakan atau membangun cache sisi klien. Solusi open source populer termasuk React Query, useSWR, dan React Router 6.4+. Anda juga dapat membangun solusi Anda sendiri, dalam hal ini Anda akan menggunakan Effects di bawah kap, tetapi juga menambahkan logika untuk mendeduplikasi permintaan, caching respons, dan menghindari air terjun(waterfalls) jaringan (dengan memuat data atau mengangkat persyaratan data ke route).
Anda dapat terus mengambil data secara langsung dalam Effects jika kedua pendekatan ini tidak cocok untuk Anda.
Menentukan dependensi yang responsif
Perhatikan bahwa Anda tidak dapat “memilih” dependensi dari Effect Anda. Setiap nilai reaktif yang digunakan oleh kode Effect Anda harus dideklarasikan sebagai dependensi. Daftar dependensi Effect Anda ditentukan oleh kode sekitarnya:
function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
}
Jika serverUrl
atau roomId
berubah, Effect Anda akan menyambung kembali ke obrolan menggunakan nilai baru.
Nilai reaktif mencakup props dan semua variabel dan fungsi yang dideklarasikan langsung di dalam komponen Anda. Karena roomId
dan serverUrl
adalah nilai reaktif, Anda tidak dapat menghapusnya dari dependensi. Jika Anda mencoba menghilangkannya dan linter Anda dikonfigurasi dengan benar untuk React, linter akan menandai ini sebagai kesalahan yang perlu Anda perbaiki:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
}
Untuk menghapus sebuah dependensi, Anda perlu “membuktikan” pada linter bahwa itu tidak perlu menjadi sebuah dependensi. Misalnya, Anda dapat memindahkan serverUrl
keluar dari komponen Anda untuk membuktikan bahwa itu tidak reaktif dan tidak akan berubah pada saat re-render:
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
Sekarang serverUrl
bukan lagi sebuah nilai yang reaktif (dan tidak bisa berubah pada re-render), sehingga tidak perlu menjadi dependensi. Jika kode Effect Anda tidak menggunakan nilai yang reaktif, daftar dependensinya harus kosong ([]
):
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
}
Sebuah Effect dengan daftar dependencies kosong tidak akan dijalankan ulang ketika props atau state dari komponen berubah.
Contoh 1 dari 3: Mengoper sebuah array dependensi
Jika kamu menentukan dependensi, Effect kamu akan dijalankan setelah render pertama dan setelah re-render dengan dependensi yang berubah.
useEffect(() => {
// ...
}, [a, b]); // Runs again if a or b are different
Dalam contoh di bawah ini, serverUrl
dan roomId
adalah nilai reaktif, sehingga keduanya harus disebutkan sebagai dependensi. Sebagai hasilnya, memilih ruangan yang berbeda dalam dropdown atau mengedit input URL server menyebabkan obrolan terhubung kembali. Namun, karena pesan
tidak digunakan dalam Effect (dan karena itu bukan dependensi), mengedit pesan tidak menyebabkan obrolan terhubung kembali.
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => { connection.disconnect(); }; }, [serverUrl, roomId]); return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> <label> Your message:{' '} <input value={message} onChange={e => setMessage(e.target.value)} /> </label> </> ); } export default function App() { const [show, setShow] = useState(false); 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> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> </label> {show && <hr />} {show && <ChatRoom roomId={roomId}/>} </> ); }
Memperbarui state berdasarkan state sebelumnya dari sebuah Effect
Ketika Anda ingin memperbarui state berdasarkan state sebelumnya dari sebuah Effect, Anda mungkin akan menghadapi masalah:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
}
Karena count
adalah nilai reaktif, ia harus disebutkan dalam daftar dependensi. Namun, hal ini menyebabkan Effect cleanup dan mengatur ulang setiap kali count
berubah. Ini tidak ideal.
Untuk mengatasinya, oper pembaruan status c => c + 1
ke setCount
:
import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const intervalId = setInterval(() => { setCount(c => c + 1); // ✅ Pass a state updater }, 1000); return () => clearInterval(intervalId); }, []); // ✅ Now count is not a dependency return <h1>{count}</h1>; }
Sekarang bahwa Anda mengoper c => c + 1
bukan count + 1
, Effect Anda tidak lagi perlu tergantung pada count
. Sebagai hasil dari perbaikan ini, ia tidak perlu cleanup dan mengatur interval lagi setiap kali count
berubah.
Menghapus dependensi objek yang tidak diperlukan
Jika Effect Anda bergantung pada objek atau fungsi yang dibuat selama rendering, mungkin berjalan terlalu sering. Misalnya, Effect ini terhubung kembali setelah setiap render karena objek opsi
berbeda untuk setiap render:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
Hindari menggunakan objek yang dibuat selama rendering sebagai dependensi. Sebaliknya, buat objek di dalam Effect:
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} /> </> ); }
Sekarang setelah Anda membuat objek opsi
di dalam Effect, Effect itu sendiri hanya tergantung pada string roomId
.
Dengan perbaikan ini, mengetik ke dalam input tidak akan menyambungkan kembali ke chat. Berbeda dengan objek yang dibuat ulang, string seperti roomId
tidak berubah kecuali Anda menetapkannya ke nilai lain. Baca lebih lanjut tentang menghapus dependensi.
Menghapus dependensi fungsi yang tidak perlu
Jika Effect Anda bergantung pada objek atau fungsi yang dibuat selama rendering, maka Effect tersebut mungkin akan berjalan terlalu sering. Misalnya, Effect ini akan terhubung kembali setelah setiap rendering karena fungsi createOptions
berbeda untuk setiap rendering:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
Jika hanya membuat sebuah fungsi dari awal pada setiap re-render, itu bukan masalah yang perlu dioptimalkan. Namun, jika Anda menggunakannya sebagai dependensi dari Effect Anda, maka akan menyebabkan Effect Anda berjalan kembali setelah setiap re-render.
Hindari menggunakan sebuah fungsi yang dibuat selama rendering sebagai dependensi. Sebaiknya, deklarasikan fungsi tersebut di dalam Effect:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; 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]); 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} /> </> ); }
Sekarang, Anda mendefinisikan fungsi createOptions
di dalam Effect, sehingga Effect itu sendiri hanya tergantung pada string roomId
. Dengan perbaikan ini, mengetik ke dalam input tidak akan membuat chat terhubung kembali. Berbeda dengan sebuah fungsi yang selalu dibuat ulang, sebuah string seperti roomId
tidak berubah kecuali jika Anda menetapkannya ke nilai lain. Baca lebih lanjut tentang cara menghapus dependensi.
Membaca props dan state terbaru dari sebuah Effect
Secara default, ketika Anda membaca sebuah nilai reaktif dari sebuah Effect, Anda harus menambahkannya sebagai sebuah dependensi. Hal ini memastikan bahwa *Effect Anda “bereaksi” terhadap setiap perubahan dari nilai tersebut. Untuk sebagian besar dependensi, itulah perilaku yang Anda inginkan.
Namun, terkadang Anda akan ingin membaca props dan state terbaru dari sebuah Effect tanpa “bereaksi” terhadapnya. Sebagai contoh, bayangkan Anda ingin mencatat jumlah item dalam keranjang belanja untuk setiap kunjungan halaman:
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}
Bagaimana jika Anda ingin mencatat kunjungan halaman baru setelah setiap perubahan url
, tetapi tidak jika hanya shoppingCart
yang berubah? Anda tidak dapat mengabaikan shoppingCart
dari dependensi tanpa melanggar aturan reaktivitas. Namun, Anda dapat menyatakan bahwa Anda “tidak ingin” suatu kode “bereaksi” terhadap perubahan meskipun itu dipanggil dari dalam sebuah Effect. Deklarasikan sebuah Effect Event dengan useEffectEvent
Hook, dan pindahkan kode yang membaca shoppingCart
ke dalamnya:
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Effect Events tidak bersifat reaktif dan selalu harus diabaikan dari ketergantungan Effect Anda. Inilah yang memungkinkan Anda menempatkan kode non-reaktif (di mana Anda dapat membaca nilai terbaru dari beberapa props dan state) di dalamnya. Dengan membaca shoppingCart
di dalam onVisit
, Anda memastikan bahwa shoppingCart
tidak akan menjalankan kembali Effect Anda.
Menampilkan konten yang berbeda di server dan klien
Jika aplikasi Anda menggunakan server rendering (baik langsung maupun melalui framework), komponen Anda akan dirender di dua lingkungan yang berbeda. Di server, komponen akan dirender untuk menghasilkan HTML awal. Di klien, React akan menjalankan kode rendering lagi sehingga ia dapat melekatkan event handler Anda ke HTML tersebut. Oleh karena itu, agar hydrasi berfungsi, output render awal Anda harus identik di klien dan server.
Dalam kasus yang jarang terjadi, Anda mungkin perlu menampilkan konten yang berbeda di klien. Misalnya, jika aplikasi Anda membaca beberapa data dari localStorage
, maka hal itu tidak mungkin dilakukan di server. Berikut adalah cara mengimplementasikannya:
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}
Ketika aplikasi sedang dimuat, pengguna akan melihat output render awal. Kemudian, setelah aplikasi selesai dimuat dan dihydrate, Effect akan berjalan dan mengatur didMount
menjadi true
, memicu re-render. Ini akan beralih ke output render khusus client. Effect tidak berjalan di server, itulah sebabnya didMount
bernilai false
selama render awal di server.
Gunakan pola ini dengan bijak. Ingatlah bahwa pengguna dengan koneksi lambat akan melihat konten awal untuk waktu yang cukup lama - potensial, beberapa detik - jadi Anda tidak ingin membuat perubahan yang terlalu drastis pada tampilan komponen Anda. Dalam banyak kasus, Anda dapat menghindari kebutuhan ini dengan menunjukkan hal-hal yang berbeda secara kondisional dengan CSS.
Penyelesaian masalah
Effect Anda berjalan dua kali saat komponen terpasang
Ketika Strict Mode aktif di pengembangan, React menjalankan setup dan cleanup satu kali ekstra sebelum setup sebenarnya.
Ini adalah stress-test yang memverifikasi logika Effect Anda diimplementasikan dengan benar. Jika ini menyebabkan masalah yang terlihat, fungsi cleanup Anda kehilangan beberapa logika. Fungsi cleanup harus menghentikan atau membatalkan apa pun yang dilakukan fungsi setup. Aturan praktisnya adalah pengguna tidak boleh dapat membedakan antara setup yang dipanggil sekali (seperti dalam produksi) dan urutan setup → cleanup → setup (seperti dalam pengembangan).
Baca lebih lanjut tentang bagaimana ini membantu menemukan bug dan cara memperbaiki logika Anda.
Effect Anda berjalan setelah setiap render ulang
Pertama, periksa apakah Anda telah lupa untuk menentukan array dependensi:
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!
Jika Anda telah menentukan array dependensi tetapi Effect Anda masih berjalan dalam loop, itu karena salah satu dependensi Anda berbeda pada setiap re-render.
Anda dapat men-debug masalah ini dengan secara manual mencatat dependensi Anda ke konsol:
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
Anda dapat mengklik kanan pada array dari re-render yang berbeda di konsol dan pilih “Simpan sebagai variabel global” untuk keduanya. Anda kemudian dapat menggunakan konsol browser untuk memeriksa apakah setiap dependensi dalam kedua array tersebut sama:
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...
Ketika Anda menemukan dependensi yang berbeda pada setiap re-render, biasanya Anda bisa memperbaikinya dengan salah satu cara berikut:
- Memperbarui state berdasarkan state sebelumnya dari sebuah Effect
- Menghapus dependensi objek yang tidak diperlukan
- Menghapus dependensi fungsi yang tidak diperlukan
- Membaca props dan state terbaru dari sebuah Effect
Sebagai upaya terakhir (jika metode-metode ini tidak membantu), bungkus pembuatannya dengan useMemo
atau useCallback
(untuk fungsi).
Effect Anda terus berjalan dalam siklus tak terbatas
Jika Effect Anda berjalan dalam siklus tak terbatas, dua hal ini harus benar:
- Effect Anda memperbarui beberapa state.
- State tersebut menyebabkan re-render, yang menyebabkan dependensi Effect berubah.
Sebelum Anda mulai memperbaiki masalah, tanyakan pada diri sendiri apakah Effect Anda terhubung ke sistem eksternal tertentu (seperti DOM, jaringan, widget pihak ketiga, dan sebagainya). Mengapa Effect Anda perlu menetapkan state? Apakah itu disinkronkan dengan sistem eksternal tersebut? Atau apakah Anda mencoba mengelola aliran data aplikasi Anda dengannya?
Jika tidak ada sistem eksternal, pertimbangkan untuk menghapus Effect sama sekali untuk menyederhanakan logika Anda.
Jika Anda benar-benar menyinkronkan dengan sistem eksternal, pertimbangkan mengapa dan dalam kondisi apa Effect Anda harus memperbarui state. Apakah ada sesuatu yang berubah yang mempengaruhi output visual komponen Anda? Jika Anda perlu melacak beberapa data yang tidak digunakan untuk merender, ref(yang tidak memicu re-render) mungkin lebih sesuai. Verifikasi bahwa Effect Anda tidak memperbarui state (dan memicu re-render) lebih dari yang dibutuhkan.
Akhirnya, jika Effect Anda memperbarui state pada waktu yang tepat, tetapi masih ada loop, itu karena pembaruan state tersebut menyebabkan salah satu dependensi Effect berubah. Baca cara memecahkan masalah perubahan dependensi.
Logika cleanup Anda berjalan meskipun komponen Anda tidak dilepas
Fungsi cleanup berjalan tidak hanya saat bongkar pasang, tetapi sebelum setiap re-render dengan dependensi yang berubah. Selain itu, dalam pengembangan, React menjalankan setup + cleanup satu kali tambahan segera setelah komponen dipasang.
Jika Anda memiliki kode cleanup tanpa kode setup yang sesuai, biasanya itu adalah tanda masalah dalam kode:
useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);
Logika cleanup Anda harus “simetris” dengan logika setup awal, dan harus menghentikan atau mengembalikan apa yang dilakukan oleh setup awal:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
Pelajari bagaimana siklus hidup Effect berbeda dari siklus hidup komponen.
Effect Anda melakukan sesuatu yang visual, dan Anda melihat kedipan sebelum berjalan
Jika Effect Anda harus menghalangi browser agar tidak mengecat layar, ganti useEffect
dengan useLayoutEffect
. Perhatikan bahwa ini seharusnya tidak diperlukan untuk sebagian besar Effect. Anda hanya memerlukannya jika sangat penting untuk menjalankan Effect Anda sebelum browser melukis: misalnya, untuk mengukur dan memposisikan tooltip sebelum pengguna melihatnya.