Tutorial
5 Kesalahan Pakai React useEffect yang Diam-diam Bikin Web Lu Lemot
Pelajari 5 anti-pattern useEffect yang memicu double re-render, race condition, dan memory leak, lengkap dengan solusinya.

Berapa kali lu nemu komponen React yang responsnya kerasa "berat", input text-nya agak laggy, atau kipas laptop mendadak muter kencang pas buka halaman tertentu? Pas lu buka React DevTools Profiler, layarnya penuh sama baris hijau dan kuning: puluhan re-render terjadi beruntun hanya untuk satu klik tombol sederhana.
Tersangka utamanya hampir selalu sama: penggunaan `useEffect` yang serampangan.
Waktu React Hooks pertama kali rilis di versi 16.8, banyak developer langsung menganggap useEffect sebagai pengganti saklek untuk componentDidMount, componentDidUpdate, dan componentWillUnmount. Pola pikir lifecycle era class component ini dibawa mentah-mentah ke paradigma deklaratif React modern. Akibatnya, useEffect sering dijadikan "tempat sampah serbaguna" untuk sinkronisasi state, perhitungan data, manipulasi DOM, hingga event handling.
Di artikel ini, kita bakal bongkar 5 kesalahan paling umum pemakaian useEffect yang bikin performa web lu anjlok, lengkap dengan analisis teknis mengapa hal itu terjadi dan bagaimana cara memperbaikinya dengan pendekatan modern.
1. Menghitung Derived State Menggunakan useEffect

Ini adalah dosa nomor satu yang paling sering ditemui di codebase produksi. Developer punya satu state utama (misalnya daftar produk), lalu mereka membuat state kedua (daftar produk terfilter), dan menghubungkan keduanya lewat useEffect.
Mari kita lihat contoh implementasi yang keliru:
// ❌ ANTI-PATTERN: Menyimpan derived state dan menyinkronkannya lewat useEffect
import { useState, useEffect } from 'react';
interface Product {
id: string;
name: string;
price: number;
category: string;
}
export function ProductListBad({ products }: { products: Product[] }) {
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);
useEffect(() => {
// Memfilter data dan memicu render ulang
if (selectedCategory === 'all') {
setFilteredProducts(products);
} else {
setFilteredProducts(products.filter((p) => p.category === selectedCategory));
}
}, [products, selectedCategory]);
return (
<div>
<select onChange={(e) => setSelectedCategory(e.target.value)}>
<option value="all">Semua</option>
<option value="electronics">Elektronik</option>
</select>
<ul>
{filteredProducts.map((p) => (
<li key={p.id}>{p.name} - Rp{p.price}</li>
))}
</ul>
</div>
);
}Mengapa Kode di Atas Merusak Performa?
Mari kita bedah siklus eksekusi di atas:
- User mengganti kategori melalui dropdown.
setSelectedCategorydipanggil. React melakukan render pertama. Di fase ini,filteredProductsmasih berisi data lama.- DOM di-commit dan browser menggambar frame pertama (stale UI).
- Selesai render, React mengeksekusi
useEffect. - Di dalam effect,
setFilteredProductsdipanggil dengan data baru. - React dipaksa melakukan render kedua (second render pass) untuk menampilkan data hasil filter.
Lu baru saja membuat React me-render komponen yang sama dua kali untuk setiap perubahan interaksi user. Kalau hierarki komponen lu dalam atau punya ratusan item, layout shift dan penurunan frame rate (FPS drop) bakal langsung terasa.
Solusi: Hitung Langsung Saat Render (Derived State)
Data yang bisa dihitung dari props atau state yang sudah ada tidak boleh disimpan di state terpisah.
Berikut perbaikan kodenya:
// ✅ BEST PRACTICE: Derived state dihitung langsung saat proses rendering
import { useState, useMemo } from 'react';
interface Product {
id: string;
name: string;
price: number;
category: string;
}
export function ProductListGood({ products }: { products: Product[] }) {
const [selectedCategory, setSelectedCategory] = useState<string>('all');
// Jika komputasi ringan (< 1000 item), hitung langsung:
const filteredProducts = selectedCategory === 'all'
? products
: products.filter((p) => p.category === selectedCategory);
// Jika komputasi sangat berat (ribuan item atau filtering kompleks), gunakan useMemo:
// const filteredProducts = useMemo(() => {
// return selectedCategory === 'all'
// ? products
// : products.filter((p) => p.category === selectedCategory);
// }, [products, selectedCategory]);
return (
<div>
<select onChange={(e) => setSelectedCategory(e.target.value)}>
<option value="all">Semua</option>
<option value="electronics">Elektronik</option>
</select>
<ul>
{filteredProducts.map((p) => (
<li key={p.id}>{p.name} - Rp{p.price}</li>
))}
</ul>
</div>
);
}Aturan Praktis: Jangan pernah menggunakan useEffect hanya untuk mengubah state lokal berdasarkan state lain. Hitung langsung nilainya di badan fungsi komponen (render body).2. Menangani User Event di dalam useEffect
Sering kali developer ingin menjalankan aksi tertentu setelah user menekan tombol—misalnya mengirim analitik, menampilkan toast notifikasi, atau mengarahkan halaman—tetapi mereka malah menyimpannya di state lalu "menangkapnya" via useEffect.
// ❌ ANTI-PATTERN: Menggunakan state flag untuk mentrigger side effect di useEffect
import { useState, useEffect } from 'react';
export function CheckoutButtonBad({ cartId }: { cartId: string }) {
const [isSubmitted, setIsSubmitted] = useState(false);
useEffect(() => {
if (isSubmitted) {
// Mengirim analitik dan notifikasi
navigator.sendBeacon('/api/analytics', JSON.stringify({ event: 'checkout_click', cartId }));
showNotification('Sedang memproses pesanan...');
setIsSubmitted(false); // Mereset state (memicu render ulang ke-3!)
}
}, [isSubmitted, cartId]);
return (
<button onClick={() => setIsSubmitted(true)}>
Bayar Sekarang
</button>
);
}Mengapa Pola Ini Buruk?
Kode ini memecah alur logika yang seharusnya imperatif (reaksi langsung terhadap aksi klik) menjadi rantai reaktif yang berputar-putar:
- Klik tombol -> ubah state
isSubmittedjaditrue(Render 1). - Effect mendeteksi perubahan -> jalankan analitik -> ubah state
isSubmittedjadifalse(Render 2). - Komponen harus melewati dua siklus render hanya untuk mengeksekusi satu fungsi pengiriman analitik.
- Logika bisnis jadi sulit di-trace karena tersebar di antara handler dan effect.
Solusi: Pindahkan Logika ke Event Handler
useEffect diciptakan untuk melakukan sinkronisasi komponen dengan sistem eksternal (external system synchronization) berdasarkan state aplikasi saat ini, bukan untuk merespons interaksi sesaat dari user.
// ✅ BEST PRACTICE: Taruh side effect langsung di dalam Event Handler
export function CheckoutButtonGood({ cartId }: { cartId: string }) {
const handleCheckout = () => {
// 1. Eksekusi side-effect imperatif seketika
navigator.sendBeacon('/api/analytics', JSON.stringify({ event: 'checkout_click', cartId }));
showNotification('Sedang memproses pesanan...');
// 2. Jalankan mutasi atau navigasi yang diperlukan
processPayment(cartId);
};
return (
<button onClick={handleCheckout}>
Bayar Sekarang
</button>
);
}Dengan menaruh logika di event handler, lu menghemat siklus render yang tidak perlu dan membuat alur kode lu jauh lebih gampang dibaca oleh engineer lain.
3. Fetching Data Tanpa AbortController dan Cleanup

Data fetching manual menggunakan useEffect dan fetch biasa rentan terhadap dua masalah kritis: Race Condition dan Memory Leak.
Bayangkan skenario di mana user berpindah tab atau mengetik filter pencarian dengan cepat. Request pertama (lambat) selesai setelah request kedua (cepat). Hasilnya, data usang menimpa data terbaru di layar.
Solusi Teknis: Gunakan AbortController
Jika lu terpaksa melakukan data fetching menggunakan native useEffect tanpa library pihak ketiga, selalu sediakan cleanup function menggunakan AbortController.
// ✅ BEST PRACTICE: Fetching dengan AbortController dan Cleanup Function
import { useState, useEffect } from 'react';
interface UserProfile {
id: string;
username: string;
bio: string;
}
export function UserProfileViewer({ userId }: { userId: string }) {
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// 1. Inisialisasi controller pembatalan
const controller = new AbortController();
const { signal } = controller;
async function fetchUserProfile() {
setLoading(true);
setError(null);
try {
const response = await fetch(`https://api.example.com/users/${userId}`, { signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: UserProfile = await response.json();
setProfile(data);
} catch (err: unknown) {
// Abaikan error jika request dibatalkan secara sengaja oleh cleanup
if (err instanceof Error && err.name === 'AbortError') {
return;
}
setError(err instanceof Error ? err.message : 'Terjadi kesalahan');
} finally {
if (!signal.aborted) {
setLoading(false);
}
}
}
fetchUserProfile();
// 2. Cleanup function: batalkan request yang sedang berjalan saat userId berganti atau komponen unmount
return () => {
controller.abort();
};
}, [userId]);
if (loading) return <p>Memuat profil...</p>;
if (error) return <p className="text-red-500">{error}</p>;
if (!profile) return null;
return (
<div>
<h1>{profile.username}</h1>
<p>{profile.bio}</p>
</div>
);
}Penjelasan Baris per Baris:
- Baris 17:
const controller = new AbortController()membuat instance baru yang mengontrol siklus hidup request. - Baris 24: Properti
{ signal }diteruskan ke parameterfetch. Browser sekarang tahu request ini bisa dibatalkan sewaktu-waktu. - Baris 31-33: Saat
controller.abort()dipanggil, fetch melempar error denganname === 'AbortError'. Kita menangkapnya agar tidak menampilkan pesan error palsu ke UI. - Baris 42-44: Cleanup function dijalankan oleh React sebelum effect berikutnya dieksekusi atau saat komponen unmount. Ini memutus koneksi HTTP yang menggantung dan mencegah race condition.
Rekomendasi Senior: Untuk aplikasi skala menengah ke atas, hindari menulis boilerplate useEffect untuk fetching. Gunakan library server-state yang matang seperti TanStack Query (React Query) atau SWR, atau manfaatkan React Server Components (RSC) jika menggunakan Next.js / Remix.4. Object & Function Literal di Dependency Array
Javascript mengevaluasi kesetaraan objek dan fungsi berdasarkan referensi memori (referential equality), bukan berdasarkan isinya. Mengirimkan objek literal atau fungsi inline ke dependency array useEffect adalah jalan tol menuju infinite re-render loop.
// ❌ ANTI-PATTERN: Objek baru dibuat di setiap render pass
import { useState, useEffect } from 'react';
export function DashboardBad({ organizationId }: { organizationId: string }) {
const [data, setData] = useState<any>(null);
// Objek options ini SELALU mendapat referensi memori baru di setiap render
const fetchOptions = {
orgId: organizationId,
timestamp: Date.now(),
};
useEffect(() => {
fetchMetrics(fetchOptions).then(res => setData(res));
}, [fetchOptions]); // 💥 Infinite Loop: fetchOptions selalu dianggap berbeda!
return <div>{/* Render Dashboard */}</div>;
}Mengapa Terjadi Infinite Loop?
- Komponen me-render pertama kali.
fetchOptionsdialokasikan di alamat memori0x001.useEffectberjalan, memanggil API, lalu mengeksekusisetData.setDatamemicu render ulang.- Komponen me-render kedua kali.
fetchOptionsdibuat ulang di alamat memori0x002. - React membandingkan dependency:
0x001 !== 0x002(berbeda!). useEffectjalan lagi -> panggilsetData-> loop berulang tanpa henti hingga browser crash.
Solusi: Sederhanakan Dependency ke Tipe Primitif atau Pindahkan ke Dalam Effect
// ✅ BEST PRACTICE: Pindahkan objek/fungsi ke dalam effect atau gunakan primitive dependency
import { useState, useEffect } from 'react';
export function DashboardGood({ organizationId }: { organizationId: string }) {
const [data, setData] = useState<any>(null);
useEffect(() => {
// Deklarasikan objek di DALAM effect jika hanya dipakai di sini
const fetchOptions = {
orgId: organizationId,
timeout: 5000,
};
let isMounted = true;
fetchMetrics(fetchOptions).then((res) => {
if (isMounted) setData(res);
});
return () => {
isMounted = false;
};
}, [organizationId]); // ✅ Aman: organizationId adalah string primitif
return <div>{/* Render Dashboard */}</div>;
}Jika lu membutuhkan fungsi callback yang di-pass dari parent, bungkus fungsi tersebut dengan useCallback di level parent, atau pertimbangkan untuk hanya mem-pass data primitif yang dibutuhkan fungsi tersebut.
5. Menyelaraskan State Antar Komponen Anak-Induk (Prop Drilling Reset)
Sering kali kita membuat form atau modal di komponen anak yang memiliki state lokal. Ketika user memilih item lain di parent, kita ingin me-reset state lokal di komponen anak tersebut. Pola yang sering salah kaprah adalah menggunakan useEffect untuk menimpa state lokal setiap kali props berubah.
// ❌ ANTI-PATTERN: Sinkronisasi props ke local state via useEffect
import { useState, useEffect } from 'react';
interface EditProfileProps {
initialBio: string;
userId: string;
}
export function EditProfileBad({ initialBio, userId }: EditProfileProps) {
const [bio, setBio] = useState(initialBio);
// Berbahaya: Setiap kali userId berubah, render ulang terjadi dua kali
useEffect(() => {
setBio(initialBio);
}, [userId, initialBio]);
return (
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
/>
);
}Selain memicu render ganda, pola ini rentan memunculkan bug subtle: jika user sedang mengetik dan ada background re-render dari parent yang mengubah initialBio, ketikan user bisa mendadak hilang tertimpa nilai lama.
Solusi Elegan: Gunakan Atribut key untuk Mereset Komponen
React memiliki mekanisme bawaan untuk mereset seluruh state komponen secara otomatis: cukup berikan properti key yang unik pada komponen tersebut di level parent.
// ✅ BEST PRACTICE: Manfaatkan 'key' untuk reset state secara deklaratif
// 1. Komponen Anak tetap bersih dan tidak butuh useEffect sama sekali
interface EditProfileProps {
initialBio: string;
}
export function EditProfileGood({ initialBio }: EditProfileProps) {
// State otomatis terinisialisasi ulang dari nol ketika key di parent berubah
const [bio, setBio] = useState(initialBio);
return (
<textarea
value={bio}
onChange={(e) => setBio(e.target.value)}
/>
);
}
// 2. Parent mengontrol lifecycle anak lewat 'key'
export function ProfilePage({ user }: { user: { id: string; bio: string } }) {
return (
<div>
<h2>Edit Profil</h2>
{/* Setiap kali user.id berganti, React me-mount ulang EditProfileGood */}
<EditProfileGood key={user.id} initialBio={user.bio} />
</div>
);
}Ketika nilai key berubah dari user-1 ke user-2, React akan menghancurkan (unmount) instance komponen lama beserta state-nya, lalu membuat instance baru dengan nilai initialBio yang baru. Tidak ada double render, tidak ada stale state, dan kode lu bersih tanpa sebaris pun useEffect.
Mental Model: Kapan Sebenarnya Kita Butuh useEffect?
Sebelum lu mengetik useEffect di editor, gunakan decision tree berikut sebagai panduan mental:
Tanyakan 3 pertanyaan ini secara berurutan:
- Apakah data ini bisa dihitung dari props atau state yang sudah ada?
Jika YA -> Hitung langsung di badan komponen. Gunakan useMemo hanya jika perhitungannya terbukti berat secara benchmark.
- Apakah aksi ini terjadi akibat interaksi spesifik user (klik, submit, scroll)?
Jika YA -> Pindahkan ke event handler terkait (onClick, onSubmit).
- Apakah kode ini bertujuan menyelaraskan komponen dengan sistem di luar React (DOM browser, WebSockets, Subscription, LocalStorage)?
Jika YA -> Ini adalah satu-satunya skenario valid untuk menggunakan `useEffect`. Jangan lupa sertakan cleanup function jika sistem tersebut membuka koneksi atau event listener.
Checklist Action Items untuk Tim Engineering
Sebelum merge pull request berikutnya ke branch utama, lakukan audit cepat terhadap komponen React lu:
- Hapus State Berlebih: Cari kombinasi
const [val, setVal] = useState()yang selalu diperbarui di dalamuseEffect. Ubah menjadi variabel biasa di render body. - Audit Dependency Array: Pastikan tidak ada fungsi atau objek anonim yang dibuat secara inline di dalam dependency array
useEffect. - Validasi Cleanup Function: Pastikan semua
addEventListener,setInterval,setTimeout, dan network call di dalamuseEffectmemiliki fungsi pembatalan (cleanup return). - Pasang ESLint Plugin: Aktifkan rule
eslint-plugin-react-hooksdengan konfigurasi ketat (exhaustive-deps: error) untuk mencegah stale closure dan dependency yang terlewat. - Gunakan Profiler: Buka Chrome DevTools atau React DevTools Profiler, rekam interaksi user, dan verifikasi bahwa satu klik tidak memicu lebih dari satu render pass yang tidak perlu.
Menulis kode React yang cepat bukan soal menghafal trik optimasi rumit, melainkan soal disiplin membiarkan React bekerja sesuai alur deklaratifnya tanpa dicekik oleh rantai useEffect yang tak berujung. Happy refactoring!
Butuh penyesuaian khusus untuk project Anda?
Jika situasi operasional atau arsitektur sistem bisnis Anda membutuhkan solusi kustom, diskusikan langsung bersama tim engineer kami. Anda juga bisa melihat rincian layanan vour.dev atau menghitung estimasi biaya project lebih dulu.
Mulai Project