Context API
State management global dengan Context API untuk menghindari prop drilling
Apa itu Context API?
Context API adalah fitur React untuk berbagi data antar komponen tanpa harus mengoper props secara manual di setiap level (prop drilling). Context sangat berguna untuk data yang perlu diakses oleh banyak komponen di berbagai level.
Prop Drilling Problem
Prop drilling terjadi ketika kita harus mengoper props melalui banyak level komponen yang sebenarnya tidak membutuhkan props tersebut, hanya untuk meneruskannya ke komponen di level lebih dalam.
// Prop drilling - Home tidak butuh tasks, hanya meneruskan
<Home tasks={tasks}>
<Container tasks={tasks}>
<TaskList tasks={tasks} />
</Container>
</Home>Kapan Menggunakan Context?
Gunakan Context untuk data yang bersifat "global" seperti:
- User authentication/profile
- Theme (dark/light mode)
- Language/localization
- Shopping cart
- Global UI state (modals, notifications)
Context vs Redux
Context API built-in di React dan cukup untuk kebanyakan kasus. Gunakan Redux/Zustand hanya jika Kalian butuh:
- Time-travel debugging
- Middleware yang kompleks
- State management yang sangat kompleks
Membuat TaskContext
Buat Context dan Provider
import React, { createContext, useContext } from 'react';
import useLocalStorage from '../hooks/useLocalStorage';
// Buat Context
const TaskContext = createContext();
// Custom hook untuk menggunakan context
export function useTasks() {
const context = useContext(TaskContext);
if (!context) {
throw new Error('useTasks must be used within a TaskProvider');
}
return context;
}
// Provider component
export function TaskProvider({ children }) {
const [tasks, setTasks] = useLocalStorage('tasks', []);
const addTask = (newTask) => {
setTasks([...tasks, newTask]);
};
const deleteTask = (taskId) => {
setTasks(tasks.filter(task => task.id !== taskId));
};
const toggleComplete = (taskId) => {
setTasks(tasks.map(task =>
task.id === taskId ? { ...task, completed: !task.completed } : task
));
};
const updateTask = (taskId, updates) => {
setTasks(tasks.map(task =>
task.id === taskId ? { ...task, ...updates } : task
));
};
// Computed values
const completedTasks = tasks.filter(task => task.completed).length;
const remainingTasks = tasks.length - completedTasks;
const value = {
tasks,
addTask,
deleteTask,
toggleComplete,
updateTask,
stats: {
total: tasks.length,
completed: completedTasks,
remaining: remainingTasks,
completionRate: tasks.length > 0 ? (completedTasks / tasks.length) * 100 : 0
}
};
return (
<TaskContext.Provider value={value}>
{children}
</TaskContext.Provider>
);
}Wrap App dengan Provider
Update App.jsx untuk menyediakan context:
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { TaskProvider } from './context/TaskContext';
import Navbar from './components/Navbar/Navbar';
import Home from './pages/Home/Home';
import About from './pages/About/About';
import './App.css';
function App() {
return (
<Router>
<TaskProvider>
<div className="App">
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
</TaskProvider>
</Router>
);
}
export default App;Gunakan Context di Components
Update Home component untuk menggunakan context:
import React from 'react';
import Header from '../../components/Header/Header';
import TaskForm from '../../components/TaskForm/TaskForm';
import TaskItem from '../../components/TaskItem/TaskItem';
import { useTasks } from '../../context/TaskContext';
import './Home.css';
function Home() {
const { tasks, addTask, deleteTask, toggleComplete, stats } = useTasks();
return (
<div className="home">
<Header
title="React Task Manager"
description="Kelola tugas Anda dengan mudah"
/>
<main className="container">
<div className="stats">
<p>Total: {stats.total} tugas</p>
<p>Selesai: {stats.completed}</p>
<p>Belum selesai: {stats.remaining}</p>
<p>Progress: {stats.completionRate.toFixed(1)}%</p>
</div>
<TaskForm onAddTask={addTask} />
<div className="task-list">
{tasks.length === 0 ? (
<p className="empty-message">Belum ada tugas. Tambahkan tugas baru!</p>
) : (
tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onDelete={deleteTask}
onToggleComplete={toggleComplete}
/>
))
)}
</div>
</main>
</div>
);
}
export default Home;No More Prop Drilling!
Perhatikan bagaimana kita bisa langsung mengakses tasks dan functions tanpa harus mengoper props dari App → Home. Any component di dalam TaskProvider bisa menggunakan useTasks()!
Context dengan useReducer
Untuk state management yang lebih kompleks, kombinasikan Context dengan useReducer:
import React, { createContext, useContext, useReducer } from 'react';
import useLocalStorage from '../hooks/useLocalStorage';
const TaskContext = createContext();
// Action types
const ACTIONS = {
ADD_TASK: 'ADD_TASK',
DELETE_TASK: 'DELETE_TASK',
TOGGLE_COMPLETE: 'TOGGLE_COMPLETE',
UPDATE_TASK: 'UPDATE_TASK',
SET_TASKS: 'SET_TASKS'
};
// Reducer function
function taskReducer(state, action) {
switch (action.type) {
case ACTIONS.ADD_TASK:
return [...state, action.payload];
case ACTIONS.DELETE_TASK:
return state.filter(task => task.id !== action.payload);
case ACTIONS.TOGGLE_COMPLETE:
return state.map(task =>
task.id === action.payload
? { ...task, completed: !task.completed }
: task
);
case ACTIONS.UPDATE_TASK:
return state.map(task =>
task.id === action.payload.id
? { ...task, ...action.payload.updates }
: task
);
case ACTIONS.SET_TASKS:
return action.payload;
default:
return state;
}
}
export function TaskProvider({ children }) {
const [storedTasks, setStoredTasks] = useLocalStorage('tasks', []);
const [tasks, dispatch] = useReducer(taskReducer, storedTasks);
// Sync dengan localStorage
React.useEffect(() => {
setStoredTasks(tasks);
}, [tasks, setStoredTasks]);
const addTask = (newTask) => {
dispatch({ type: ACTIONS.ADD_TASK, payload: newTask });
};
const deleteTask = (taskId) => {
dispatch({ type: ACTIONS.DELETE_TASK, payload: taskId });
};
const toggleComplete = (taskId) => {
dispatch({ type: ACTIONS.TOGGLE_COMPLETE, payload: taskId });
};
const updateTask = (taskId, updates) => {
dispatch({
type: ACTIONS.UPDATE_TASK,
payload: { id: taskId, updates }
});
};
const completedTasks = tasks.filter(task => task.completed).length;
const remainingTasks = tasks.length - completedTasks;
const value = {
tasks,
addTask,
deleteTask,
toggleComplete,
updateTask,
stats: {
total: tasks.length,
completed: completedTasks,
remaining: remainingTasks,
completionRate: tasks.length > 0 ? (completedTasks / tasks.length) * 100 : 0
}
};
return (
<TaskContext.Provider value={value}>
{children}
</TaskContext.Provider>
);
}
export function useTasks() {
const context = useContext(TaskContext);
if (!context) {
throw new Error('useTasks must be used within a TaskProvider');
}
return context;
}useState vs useReducer
Gunakan useState untuk state yang sederhana, gunakan useReducer untuk:
- State yang kompleks dengan banyak sub-values
- Logika update yang kompleks
- State yang tergantung pada state sebelumnya
Multiple Contexts
Kalian bisa menggunakan multiple contexts untuk different concerns:
import React, { createContext, useContext, useState } from 'react';
const AuthContext = createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const login = (username, password) => {
// Implement login logic
setUser({ username });
setIsAuthenticated(true);
};
const logout = () => {
setUser(null);
setIsAuthenticated(false);
};
const value = {
user,
isAuthenticated,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}Menggunakan Multiple Providers
import { TaskProvider } from './context/TaskContext';
import { AuthProvider } from './context/AuthContext';
function App() {
return (
<AuthProvider>
<TaskProvider>
<Router>
{/* App content */}
</Router>
</TaskProvider>
</AuthProvider>
);
}Context dengan TypeScript
Jika menggunakan TypeScript, definisikan types dengan jelas:
import React, { createContext, useContext, ReactNode } from 'react';
interface Task {
id: number;
title: string;
completed: boolean;
}
interface TaskContextType {
tasks: Task[];
addTask: (task: Task) => void;
deleteTask: (id: number) => void;
toggleComplete: (id: number) => void;
stats: {
total: number;
completed: number;
remaining: number;
completionRate: number;
};
}
const TaskContext = createContext<TaskContextType | undefined>(undefined);
export function useTasks() {
const context = useContext(TaskContext);
if (!context) {
throw new Error('useTasks must be used within a TaskProvider');
}
return context;
}
interface TaskProviderProps {
children: ReactNode;
}
export function TaskProvider({ children }: TaskProviderProps) {
// Implementation...
}Performance Optimization
Context dapat menyebabkan unnecessary re-renders. Optimize dengan:
1. Split Contexts
// Satu context untuk semua
<AppContext.Provider value={{ user, theme, tasks, ... }}>
// Pisah per concern
<UserContext.Provider>
<ThemeContext.Provider>
<TaskContext.Provider>2. Memoize Context Value
import { useMemo } from 'react';
export function TaskProvider({ children }) {
const [tasks, setTasks] = useState([]);
const value = useMemo(() => ({
tasks,
addTask: (task) => setTasks([...tasks, task]),
deleteTask: (id) => setTasks(tasks.filter(t => t.id !== id))
}), [tasks]);
return (
<TaskContext.Provider value={value}>
{children}
</TaskContext.Provider>
);
}3. Split State and Actions
const TaskStateContext = createContext();
const TaskActionsContext = createContext();
// Components yang hanya butuh actions tidak re-render saat state berubah
const actions = useMemo(() => ({
addTask,
deleteTask,
toggleComplete
}), []);Best Practices
- Create custom hooks -
useTasks()lebih baik dari useContext(TaskContext) - Validate context - Throw error jika digunakan di luar Provider
- Keep context focused - Satu context untuk satu concern
- Memoize values - Gunakan useMemo untuk prevent unnecessary renders
- Don't overuse - Context bukan pengganti semua props
- TypeScript - Gunakan TypeScript untuk type safety
Latihan
- Buat ThemeContext untuk dark/light mode
- Buat NotificationContext untuk global notifications
- Buat FilterContext untuk filter tasks (All, Active, Completed)
- Implement authentication dengan AuthContext
- Optimize context untuk prevent unnecessary re-renders
Apa Selanjutnya?
Selanjutnya kita akan belajar Testing dengan Jest dan React Testing Library!
