Table of Contents
▼- 1. Prop Drilling yang Tidak Perlu dan Kapan Context API Bukan Solusi yang Tepat
- 2. Menyimpan Data Server di State Klien yang Memperlambat Aplikasi
- 3. Overengineering dengan Redux saat useState dan useReducer Sudah Cukup
- 4. Derived State yang Salah Tempat antara Kalkulasi di Render vs di Selector
- 5. Strategi State Management yang Realistis untuk Tim Indonesia
- Kesimpulan
Banyak developer React Indonesia yang bisa membangun fitur dengan cepat, tapi mulai kewalahan ketika aplikasi tumbuh ke skala medium — puluhan komponen, data dari beberapa endpoint, dan bug yang muncul dari arah yang tidak terduga.
Masalahnya bukan di library. Bukan juga karena kurang hafal dokumentasi React.
Masalahnya ada di pola pikir saat mengelola state.
Artikel ini bukan review library state management. Ini adalah audit jujur terhadap kesalahan pola yang paling sering dilakukan developer React — kesalahan yang tidak langsung meledak, tapi pelan-pelan membuat codebase makin sulit dipahami dan di-debug.
1. Prop Drilling yang Tidak Perlu dan Kapan Context API Bukan Solusi yang Tepat
Prop drilling — yaitu mengoper props dari komponen induk ke anak, ke cucu, ke cicit — adalah keluhan klasik di React.
Solusi yang paling sering diambil? Langsung pakai Context API.
Tapi ini justru kesalahan pertama yang banyak terjadi.
Context API bukan pengganti prop drilling secara umum. Context tepat digunakan untuk data yang benar-benar global dan jarang berubah, seperti tema (dark/light mode), bahasa (i18n), atau data user yang sudah login.
Masalah muncul ketika developer menyimpan state yang sering berubah — seperti filter produk, input form, atau status loading — di dalam Context.
Setiap kali nilai Context berubah, semua komponen yang mengonsumsi Context tersebut akan re-render, meskipun mereka tidak menggunakan nilai yang berubah.
// ❌ Kesalahan: Context untuk state yang sering berubah
const CartContext = createContext();
function CartProvider({ children }) {
const [cartItems, setCartItems] = useState([]);
const [filterKeyword, setFilterKeyword] = useState(''); // berubah setiap ketikan
return (
<CartContext.Provider value={{ cartItems, filterKeyword, setFilterKeyword }}>
{children}
</CartContext.Provider>
);
}
Dalam contoh di atas, komponen CheckoutButton yang hanya butuh cartItems akan ikut re-render setiap kali user mengetik di kolom pencarian — karena filterKeyword ada di Context yang sama.
Solusi yang lebih realistis: pisahkan Context berdasarkan frekuensi perubahan, atau turunkan state ke level komponen yang paling rendah yang membutuhkannya (state colocation).
Jika prop drilling hanya melewati 1-2 level, biarkan saja. Itu bukan masalah. Itu justru explicit dan mudah di-trace.
2. Menyimpan Data Server di State Klien yang Memperlambat Aplikasi
Ini adalah kesalahan yang paling sering ditemukan di codebase React skala medium di Indonesia.
Polanya seperti ini:
// ❌ Anti-pattern umum
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, []);
}
Tidak ada yang salah secara sintaksis di sini. Tapi ada masalah arsitektur yang serius.
Developer sedang menggunakan useState untuk menyimpan data yang asalnya dari server — dan ikut menanggung semua beban manajemen siklus hidupnya: loading, error, cache, refetch, stale data.
Ketika ada dua komponen yang butuh data produk yang sama, developer akan copy-paste logic yang sama. Atau mencoba angkat state ke atas (lift state up), yang membuat komponen parent jadi berat dan penuh logika yang sebenarnya bukan urusannya.
Data dari server bukan client state.
Data server punya siklus hidup sendiri: kapan di-fetch, kapan expired, kapan perlu di-refetch. Mengelola ini dengan useState + useEffect berarti kamu menulis ulang cache layer secara manual — dan biasanya tidak sempurna.
Solusi yang tepat adalah menggunakan library yang memang dirancang untuk server state, seperti TanStack Query (React Query) atau SWR.
// ✅ Dengan TanStack Query
import { useQuery } from '@tanstack/react-query';
function ProductList() {
const { data: products, isLoading, error } = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(res => res.json()),
});
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage />;
return <ul>{products.map(p => <ProductItem key={p.id} product={p} />)}</ul>;
}
Lebih pendek, lebih jelas, dan caching ditangani secara otomatis. Komponen lain yang butuh data yang sama tinggal memanggil useQuery dengan queryKey yang sama — tidak ada duplikasi fetch.
3. Overengineering dengan Redux saat useState dan useReducer Sudah Cukup
Redux bukan musuh. Redux adalah alat yang tepat untuk masalah yang tepat.
Masalahnya, banyak developer Indonesia yang langsung setup Redux Toolkit di hari pertama proyek — bahkan untuk aplikasi yang hanya punya 3-4 halaman dan tidak ada state yang perlu dishare secara global.
Akibatnya? Boilerplate yang besar, learning curve yang curam untuk anggota tim baru, dan overhead mental yang tidak sebanding dengan manfaatnya.
Kapan useState sudah cukup?
Ketika state hanya relevan untuk satu komponen atau subtree kecil, useState adalah pilihan yang benar. Form lokal, toggle UI, state pagination dalam satu halaman — semuanya bisa diselesaikan dengan useState.
Kapan useReducer lebih tepat?
Ketika state memiliki banyak sub-nilai yang saling berkaitan, atau ketika transisi state cukup kompleks (beberapa aksi bisa memicu perubahan yang sama), useReducer memberikan struktur yang lebih jelas.
// ✅ useReducer untuk state yang kompleks tapi lokal
const initialState = { step: 1, formData: {}, errors: {} };
function checkoutReducer(state, action) {
switch (action.type) {
case 'NEXT_STEP':
return { ...state, step: state.step + 1 };
case 'UPDATE_FORM':
return { ...state, formData: { ...state.formData, ...action.payload } };
case 'SET_ERRORS':
return { ...state, errors: action.payload };
default:
return state;
}
}
function CheckoutFlow() {
const [state, dispatch] = useReducer(checkoutReducer, initialState);
// ...
}
Gunakan Redux (atau Zustand) ketika state benar-benar perlu dishare di banyak bagian aplikasi yang tidak punya hubungan parent-child langsung, dan ketika pola tersebut memang muncul secara organik — bukan dari asumsi awal.
Aturan sederhananya: mulai dari yang paling simpel. Naikkan kompleksitas hanya ketika masalah nyata muncul.
4. Derived State yang Salah Tempat antara Kalkulasi di Render vs di Selector
Derived state adalah nilai yang dihitung dari state yang sudah ada. Ini adalah area di mana banyak bug halus lahir.
Kesalahan pertama: menyimpan derived state sebagai state terpisah.
// ❌ Kesalahan: derived state yang disimpan manual
const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0); // ini derived state!
function addItem(item) {
const newItems = [...cartItems, item];
setCartItems(newItems);
setTotalPrice(newItems.reduce((sum, i) => sum + i.price, 0)); // harus diupdate manual
}
Setiap kali cartItems berubah, kamu harus ingat untuk update totalPrice juga. Ini adalah sumber bug yang sangat mudah muncul — terutama ketika ada banyak tempat yang memodifikasi cartItems.
Derived state tidak perlu disimpan di state. Hitung langsung saat render.
// ✅ Benar: hitung saat dibutuhkan
const [cartItems, setCartItems] = useState([]);
const totalPrice = cartItems.reduce((sum, item) => sum + item.price, 0);
React cukup pintar. Selama cartItems tidak berubah, totalPrice akan konsisten. Dan kamu tidak perlu menjaga sinkronisasi dua state secara manual.
Untuk kalkulasi yang mahal secara komputasi, gunakan useMemo agar tidak dihitung ulang di setiap render yang tidak relevan.
// ✅ useMemo untuk kalkulasi berat
const totalPrice = useMemo(
() => cartItems.reduce((sum, item) => sum + item.price, 0),
[cartItems]
);
Kesalahan kedua yang lebih halus: melakukan transformasi data dari server (filtering, sorting, grouping) langsung di dalam JSX tanpa memoize.
// ❌ Transformasi berat langsung di JSX
return (
<ul>
{products
.filter(p => p.category === activeCategory)
.sort((a, b) => b.rating - a.rating)
.map(p => <ProductCard key={p.id} product={p} />)}
</ul>
);
Setiap re-render — bahkan yang dipicu oleh perubahan state yang tidak berhubungan — akan menjalankan filter dan sort ulang terhadap seluruh array produk.
Pindahkan logika ini ke useMemo dengan dependency yang tepat, atau gunakan selector jika kamu sudah pakai Redux Toolkit.
5. Strategi State Management yang Realistis untuk Tim Indonesia
Setelah membahas empat kesalahan di atas, pertanyaan yang wajar muncul: jadi sebaiknya pakai apa?
Jawabannya tidak ada yang universal. Tapi ada kerangka berpikir yang bisa langsung dipakai.
Kategorikan state berdasarkan asalnya:
- UI State (toggle, modal open/close, tab aktif) →
useStateatauuseReducerlokal - Server State (data dari API) → TanStack Query atau SWR
- Form State (input form yang kompleks) → React Hook Form atau
useReducerlokal - Global App State (user yang login, cart, preferensi) → Zustand atau Context yang dibatasi scope-nya
Zustand sebagai alternatif Redux yang lebih ringan:
Untuk tim kecil-menengah di Indonesia yang tidak butuh DevTools se-canggih Redux, Zustand adalah pilihan yang sangat pragmatis. Setup minimal, API intuitif, dan tidak memaksa struktur yang kaku.
// Setup Zustand — sangat ringkas
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })),
}));
Investasikan waktu di state architecture, bukan di library exploration.
Banyak waktu developer Indonesia habis untuk berdebat soal library — Redux vs MobX vs Zustand vs Jotai. Padahal masalah terbesarnya bukan di pilihan library, tapi di kurangnya kesadaran tentang kategori state dan di mana seharusnya state itu hidup.
Sebelum memilih library, jawab dulu tiga pertanyaan ini:
- Siapa yang butuh state ini? Hanya komponen ini, subtree ini, atau seluruh aplikasi?
- Dari mana state ini berasal? Dari user interaction atau dari server?
- Seberapa sering state ini berubah? Setiap keystroke atau hanya sekali saat login?
Jawaban dari tiga pertanyaan itu sudah cukup untuk menentukan strategi yang tepat — bahkan sebelum kamu membuka dokumentasi library manapun.
Kesulitan dengan tugas programming atau butuh bantuan coding? KerjaKode siap membantu menyelesaikan tugas IT dan teknik informatika Anda. Dapatkan bantuan profesional di jasa tugas IT KerjaKode.
Kesimpulan
State management di React bukan tentang hafal API. Ini tentang kebiasaan berpikir.
Lima kesalahan yang dibahas di sini — prop drilling yang sembrono, menyimpan server data di client state, overengineering dengan Redux, derived state yang tidak perlu, dan kurangnya kategorisasi state — semuanya berakar dari kebiasaan yang sama: mengambil solusi sebelum benar-benar memahami masalahnya.
Kode React yang bersih bukan kode yang pakai library paling canggih. Tapi kode yang state-nya hidup di tempat yang paling tepat, diperbarui dengan cara yang paling predictable, dan bisa di-debug tanpa harus mengejar alur data melintasi belasan file.
Mulai dari yang paling simpel. Tambah kompleksitas hanya ketika masalah nyata muncul. Dan kategorikan state sebelum memilih alat.
Itu saja — dan sebagian besar bug state management yang membingungkan akan hilang dengan sendirinya.