Langsung ke konten
KamusNgoding
Menengah React 6 menit baca

React Context vs Redux: Perbedaan dan Kapan Menggunakannya

#react #state management #context api #redux

React Context vs Redux: Perbedaan dan Kapan Menggunakannya

Pendahuluan

Salah satu tantangan terbesar dalam membangun aplikasi React yang besar adalah manajemen state. Ketika aplikasimu mulai berkembang, kamu akan menemui situasi di mana data perlu diakses oleh banyak komponen yang tersebar di berbagai level hierarki komponen. Dua solusi yang paling sering dibahas adalah React Context dan Redux.

Bayangkan kamu ingin membangun aplikasi e-commerce seperti Tokopedia — kamu punya data pengguna yang perlu diakses di navbar, halaman profil, dan checkout secara bersamaan. Tanpa manajemen state yang tepat, kamu akan terjebak dalam masalah prop drilling yang menyakitkan.

Artikel ini akan membantu kamu memahami kapan harus menggunakan React Context, kapan harus beralih ke Redux, dan bagaimana cara mengimplementasikan keduanya dengan benar. Pastikan kamu sudah memahami dasar-dasar JavaScript modern seperti yang dibahas di Menguasai Array Methods: Map, Filter, dan Reduce di JavaScript, karena konsep-konsep tersebut akan sering muncul dalam kode Redux.


Konsep Dasar

Apa Itu Prop Drilling?

Sebelum membahas solusinya, mari pahami masalahnya. Prop drilling terjadi saat kamu harus mengoper data dari komponen induk ke komponen cucu melewati komponen perantara yang sebenarnya tidak butuh data tersebut.

// Masalah: prop drilling yang menyakitkan
function App() {
  const [user, setUser] = useState({ name: "Budi", role: "admin" });
  return <Dashboard user={user} />;
}

function Dashboard({ user }) {
  // Dashboard tidak butuh user, tapi harus mengopernya
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  // Sidebar pun sama
  return <UserProfile user={user} />;
}

function UserProfile({ user }) {
  // Baru di sini data digunakan
  return <p>Halo, {user.name}!</p>;
}

React Context API

React Context adalah solusi bawaan React untuk berbagi data antar komponen tanpa prop drilling. Ia bekerja seperti “variabel global” yang hanya tersedia dalam lingkup komponen tertentu.

Kapan pakai Context?

  • State yang jarang berubah (tema, bahasa, data user yang sudah login)
  • Aplikasi kecil hingga menengah
  • Kamu ingin solusi tanpa dependensi eksternal

Kelemahan Context:

  • Setiap komponen yang mengonsumsi context akan re-render saat nilai context berubah, meskipun data yang berubah bukan yang mereka butuhkan
  • Tidak ada alat debugging yang canggih
  • Tidak ideal untuk state yang sering berubah dan kompleks

Redux

Redux adalah library manajemen state eksternal yang mengikuti pola unidirectional data flow. Ia menggunakan konsep store (satu tempat penyimpanan state), action (deskripsi perubahan), dan reducer (fungsi murni yang mengubah state).

Kapan pakai Redux?

  • Aplikasi besar dengan state yang kompleks
  • State yang sering berubah dan perlu diakses banyak komponen
  • Kamu butuh time-travel debugging dan logging yang canggih
  • Tim besar yang butuh standar pengelolaan state yang konsisten

Kelemahan Redux:

  • Boilerplate code yang lebih banyak
  • Kurva belajar lebih curam
  • Overkill untuk aplikasi kecil

Contoh Kode

Implementasi React Context

Mari kita buat sistem autentikasi sederhana menggunakan Context. Semua file berikut bisa langsung dijalankan bersama:

// AuthContext.jsx
import { createContext, useContext, useState, useMemo } from "react";

// 1. Buat Context
const AuthContext = createContext(null);

// 2. Buat Provider
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (userData) => {
    setUser(userData);
  };

  const logout = () => {
    setUser(null);
  };

  // useMemo mencegah re-render tidak perlu pada consumer
  const value = useMemo(() => ({ user, login, logout }), [user]);

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// 3. Buat custom hook untuk kemudahan akses
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth harus digunakan di dalam AuthProvider");
  }
  return context;
}
// App.jsx
import { AuthProvider } from "./AuthContext";
import Navbar from "./Navbar";
import Dashboard from "./Dashboard";

function App() {
  return (
    <AuthProvider>
      <Navbar />
      <Dashboard />
    </AuthProvider>
  );
}

export default App;
// Navbar.jsx — langsung akses tanpa prop drilling!
import { useAuth } from "./AuthContext";

function Navbar() {
  const { user, logout } = useAuth();

  return (
    <nav>
      {user ? (
        <>
          <span>Halo, {user.name}!</span>
          <button onClick={logout}>Keluar</button>
        </>
      ) : (
        <span>Silakan login</span>
      )}
    </nav>
  );
}

export default Navbar;
// Dashboard.jsx
import { useAuth } from "./AuthContext";

function Dashboard() {
  const { user, login } = useAuth();

  const handleLogin = () => {
    login({ name: "Budi", role: "admin" });
  };

  return (
    <div>
      <h1>Dashboard</h1>
      {user ? (
        <p>Selamat datang, {user.name}! Role kamu: {user.role}</p>
      ) : (
        <button onClick={handleLogin}>Login sebagai Budi</button>
      )}
    </div>
  );
}

export default Dashboard;

Implementasi Redux Toolkit

Redux modern menggunakan Redux Toolkit yang mengurangi boilerplate secara signifikan. Kita akan membuat fitur keranjang belanja yang lengkap:

npm install @reduxjs/toolkit react-redux
// store/cartSlice.js
import { createSlice, createSelector } from "@reduxjs/toolkit";

const cartSlice = createSlice({
  name: "cart",
  initialState: {
    items: [],
    totalQuantity: 0,
  },
  reducers: {
    // Redux Toolkit menggunakan Immer, jadi mutasi langsung aman di sini
    addItem(state, action) {
      const existingItem = state.items.find(
        (item) => item.id === action.payload.id
      );
      if (existingItem) {
        existingItem.quantity += 1;
      } else {
        state.items.push({ ...action.payload, quantity: 1 });
      }
      state.totalQuantity += 1;
    },
    removeItem(state, action) {
      const itemIndex = state.items.findIndex(
        (item) => item.id === action.payload
      );
      if (itemIndex !== -1) {
        if (state.items[itemIndex].quantity === 1) {
          state.items.splice(itemIndex, 1);
        } else {
          state.items[itemIndex].quantity -= 1;
        }
        state.totalQuantity -= 1;
      }
    },
  },
});

export const { addItem, removeItem } = cartSlice.actions;

// Selector dengan memoization menggunakan createSelector
export const selectTotalQuantity = (state) => state.cart.totalQuantity;
export const selectAllItems = (state) => state.cart.items;
export const selectExpensiveItems = createSelector(
  selectAllItems,
  (items) => items.filter((item) => item.price > 100000)
);

export default cartSlice.reducer;
// store/index.js
import { configureStore } from "@reduxjs/toolkit";
import cartReducer from "./cartSlice";

export const store = configureStore({
  reducer: {
    cart: cartReducer,
  },
});
// App.jsx
import { Provider } from "react-redux";
import { store } from "./store";
import CartIcon from "./CartIcon";
import ProductList from "./ProductList";

function App() {
  return (
    <Provider store={store}>
      <CartIcon />
      <ProductList />
    </Provider>
  );
}

export default App;
// CartIcon.jsx — membaca state dari Redux store
import { useSelector } from "react-redux";
import { selectTotalQuantity } from "./store/cartSlice";

function CartIcon() {
  const totalQuantity = useSelector(selectTotalQuantity);

  return (
    <div style={{ position: "relative", display: "inline-block" }}>
      <span style={{ fontSize: "24px" }}>🛒</span>
      {totalQuantity > 0 && (
        <span style={{
          background: "red",
          color: "white",
          borderRadius: "50%",
          padding: "2px 6px",
          fontSize: "12px",
          position: "absolute",
          top: "-8px",
          right: "-8px",
        }}>
          {totalQuantity}
        </span>
      )}
    </div>
  );
}

export default CartIcon;
// ProductCard.jsx — dispatch action ke Redux store
import { useDispatch } from "react-redux";
import { addItem } from "./store/cartSlice";

function ProductCard({ product }) {
  const dispatch = useDispatch();

  const handleAddToCart = () => {
    dispatch(addItem({
      id: product.id,
      name: product.name,
      price: product.price,
    }));
  };

  return (
    <div style={{ border: "1px solid #ccc", padding: "16px", margin: "8px" }}>
      <h3>{product.name}</h3>
      <p>Rp {product.price.toLocaleString("id-ID")}</p>
      <button onClick={handleAddToCart}>Tambah ke Keranjang</button>
    </div>
  );
}

export default ProductCard;
// ProductList.jsx — daftar produk untuk demo
import ProductCard from "./ProductCard";

const products = [
  { id: 1, name: "Laptop Gaming", price: 15000000 },
  { id: 2, name: "Mechanical Keyboard", price: 850000 },
  { id: 3, name: "Monitor 27 inch", price: 4500000 },
];

function ProductList() {
  return (
    <div>
      <h2>Daftar Produk</h2>
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

export default ProductList;

Perbandingan Langsung

AspekReact ContextRedux
SetupMinimal, bawaan ReactPerlu instalasi library
BoilerplateRendahSedang (lebih rendah dengan Redux Toolkit)
DevToolsTidak adaRedux DevTools yang canggih
PerformanceBisa kurang optimal jika banyak updateLebih optimal dengan selector
Cocok untukState sederhana, jarang berubahState kompleks, sering berubah
Learning curveRendahSedang

Seperti membangun fitur notifikasi real-time untuk aplikasi seperti Gojek — jika notifikasinya sederhana (status login user), Context sudah cukup. Tapi jika kamu perlu mengelola ratusan state order yang terus berubah secara bersamaan, Redux adalah pilihan yang lebih tepat.


Troubleshooting: Error yang Sering Muncul

Context Value Selalu Undefined

Penyebab: Komponen mengonsumsi context di luar Provider, atau Provider belum membungkus komponen yang membutuhkannya.

Solusi:

// ❌ Salah: komponen di luar Provider
function App() {
  return (
    <>
      <UserProfile />        {/* Tidak bisa akses AuthContext! */}
      <AuthProvider>
        <Dashboard />
      </AuthProvider>
    </>
  );
}

// ✅ Benar: semua komponen yang butuh context ada di dalam Provider
function App() {
  return (
    <AuthProvider>
      <UserProfile />
      <Dashboard />
    </AuthProvider>
  );
}

Re-render Berlebihan dengan Context

Penyebab: Object value di Provider dibuat ulang setiap render, menyebabkan semua consumer ikut re-render meskipun data tidak berubah.

Solusi:

// ❌ Masalah: object baru dibuat setiap render
function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    // { user, setUser } membuat object baru setiap render!
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
}

// ✅ Solusi: gunakan useMemo untuk menstabilkan value
import { useMemo } from "react";

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const value = useMemo(() => ({ user, setUser }), [user]);
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

Redux State Tidak Terupdate di Komponen

Penyebab: Mutasi state secara langsung di reducer tanpa menggunakan Redux Toolkit (yang sudah include Immer secara otomatis).

Solusi:

// ❌ Salah: mutasi state langsung (tanpa Redux Toolkit)
const cartReducer = (state = initialState, action) => {
  if (action.type === "ADD_ITEM") {
    state.items.push(action.payload); // MUTASI LANGSUNG — React tidak mendeteksi perubahan!
    return state;
  }
  return state;
};

// ✅ Benar: selalu return state baru
const cartReducer = (state = initialState, action) => {
  if (action.type === "ADD_ITEM") {
    return {
      ...state,
      items: [...state.items, action.payload], // State baru, React mendeteksi perubahan!
    };
  }
  return state;
};

useSelector Menyebabkan Re-render Tidak Perlu

Penyebab: Selector mengembalikan object atau array baru setiap kali dipanggil, meski datanya sama secara nilai.

Solusi:

// ❌ Masalah: selector membuat array baru setiap render
const expensiveItems = useSelector(
  (state) => state.cart.items.filter((item) => item.price > 100000)
);

// ✅ Solusi: gunakan createSelector dari Redux Toolkit untuk memoization
import { createSelector } from "@reduxjs/toolkit";

const selectExpensiveItems = createSelector(
  (state) => state.cart.items,
  (items) => items.filter((item) => item.price > 100000)
  // Fungsi filter hanya dipanggil ulang jika items benar-benar berubah
);

// Di komponen:
const expensiveItems = useSelector(selectExpensiveItems);

Pertanyaan yang Sering Diajukan

Apa perbedaan utama antara React Context dan Redux?

React Context adalah fitur bawaan React yang dirancang untuk berbagi data antar komponen tanpa prop drilling, cocok untuk data yang jarang berubah. Redux adalah library eksternal dengan arsitektur yang lebih terstruktur, dilengkapi DevTools canggih, dan lebih optimal untuk aplikasi dengan state yang kompleks dan sering berubah. Singkatnya, Context untuk kesederhanaan, Redux untuk skalabilitas.

Apakah React Context bisa menggantikan Redux sepenuhnya?

Untuk aplikasi kecil hingga menengah dengan kebutuhan state sederhana, Context bisa menggantikan Redux. Namun untuk aplikasi besar dengan ratusan komponen, banyak async operation, atau kebutuhan debugging yang serius, Redux (terutama Redux Toolkit) masih menjadi pilihan yang lebih tepat karena performanya lebih optimal dan tooling-nya lebih lengkap.

Bagaimana cara memilih antara Context dan Redux untuk proyek baru?

Mulailah dengan React Context jika aplikasimu masih sederhana — prinsipnya adalah jangan over-engineer di awal. Jika kamu mulai menemukan masalah performa, logika state yang kompleks, atau butuh debugging yang lebih baik, migrasikan ke Redux Toolkit. Kamu juga bisa menggunakan keduanya bersamaan: Context untuk tema/bahasa, Redux untuk state bisnis utama.

Apakah Redux Toolkit sama dengan Redux biasa?

Redux Toolkit (RTK) adalah cara resmi dan modern untuk menulis Redux. RTK mengurangi boilerplate secara drastis, sudah include Immer untuk mutasi state yang aman, dan menyertakan utilitas seperti createSlice dan createAsyncThunk. Jika kamu baru memulai dengan Redux, langsung gunakan RTK — jangan mulai dari Redux “vanilla”.

Bisakah saya menggunakan Context dan Redux bersamaan dalam satu aplikasi?

Ya, ini adalah pendekatan yang umum dan valid. Misalnya, gunakan Context untuk data preferensi UI (tema gelap/terang, bahasa) yang jarang berubah, dan gunakan Redux untuk mengelola state bisnis yang kompleks seperti data produk, keranjang belanja, atau status transaksi. Keduanya bisa hidup berdampingan tanpa konflik.


Kesimpulan

React Context dan Redux bukan saingan — keduanya adalah alat yang berbeda untuk masalah yang berbeda. Context cocok untuk state sederhana yang jarang berubah seperti autentikasi atau preferensi tema, sementara Redux dengan Redux Toolkit adalah pilihan terbaik untuk aplikasi besar dengan logika state yang kompleks.

Panduan praktisnya: mulai dengan Context, beralih ke Redux saat kamu benar-benar membutuhkannya. Jangan lupa bahwa ada juga alternatif lain seperti Zustand atau Jotai yang menawarkan kesederhanaan Redux Toolkit dengan boilerplate yang bahkan lebih minim — worth dieksplorasi juga!

Jika kamu ingin memperdalam pemahaman JavaScript yang menjadi fondasi konsep-konsep ini, artikel Belajar Manipulasi DOM JavaScript untuk Pemula bisa menjadi referensi yang bagus untuk memahami bagaimana React bekerja di balik layar.

Selamat bereksperimen dengan state management di React! Ingat, pilihan teknologi terbaik adalah yang paling sesuai dengan kebutuhan proyekmu, bukan yang paling populer. Jangan ragu untuk terus eksplorasi artikel-artikel lainnya di KamusNgoding — setiap langkah kecil dalam belajar adalah investasi besar untuk karirmu sebagai developer! 💪

Artikel Terkait