React Dasar

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

src/context/TaskContext.jsx
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:

src/App.jsx
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:

src/pages/Home/Home.jsx
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:

src/context/TaskContext.jsx (with 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:

src/context/AuthContext.jsx
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

src/App.jsx
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:

src/context/TaskContext.tsx
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

  1. Create custom hooks - useTasks() lebih baik dari useContext(TaskContext)
  2. Validate context - Throw error jika digunakan di luar Provider
  3. Keep context focused - Satu context untuk satu concern
  4. Memoize values - Gunakan useMemo untuk prevent unnecessary renders
  5. Don't overuse - Context bukan pengganti semua props
  6. TypeScript - Gunakan TypeScript untuk type safety

Latihan

  1. Buat ThemeContext untuk dark/light mode
  2. Buat NotificationContext untuk global notifications
  3. Buat FilterContext untuk filter tasks (All, Active, Completed)
  4. Implement authentication dengan AuthContext
  5. Optimize context untuk prevent unnecessary re-renders

Apa Selanjutnya?

Selanjutnya kita akan belajar Testing dengan Jest dan React Testing Library!