Studi Kasus #21: Aplikasi Wisata Indonesia – Mengelola State Global dengan Context API


Studi Kasus #21: Aplikasi Wisata Indonesia – Mengelola State Global dengan Context API

Halo, calon full-stack developer!

Di Studi Kasus #20, kita telah menambahkan proteksi rute admin dengan autentikasi. Sekarang kita akan mempelajari cara mengelola state global menggunakan Context API. State global berguna untuk data yang digunakan di banyak komponen, seperti notifikasi, keranjang belanja, atau data user yang login.

Pada aplikasi Wisata Indonesia, kita akan membuat sistem notifikasi (toast) yang bisa dipanggil dari mana saja untuk menampilkan pesan sukses, error, atau info. Notifikasi akan muncul di pojok layar dan menghilang setelah beberapa detik. Selain itu, kita juga akan membuat context untuk menyimpan data user yang login agar mudah diakses di seluruh komponen.

Mari kita mulai!

Apa itu Context API?

Context API adalah fitur React yang memungkinkan kita berbagi state ke banyak komponen tanpa harus mengoper props secara manual di setiap level (prop drilling). Context terdiri dari:

  • Context object – dibuat dengan React.createContext().
  • Provider – komponen yang menyediakan nilai context ke semua child.
  • Consumer – komponen yang menggunakan nilai context (atau dengan hook useContext).

Kita akan membuat dua context: NotificationContext untuk notifikasi, dan AuthContext untuk data autentikasi.

Langkah 1: Membuat NotificationContext

Buat folder src/contexts jika belum ada. Di dalamnya, buat file NotificationContext.jsx.

import React, { createContext, useContext, useState, useCallback } from 'react';

// Membuat context
const NotificationContext = createContext();

// Hook kustom untuk menggunakan context
export const useNotification = () => {
  const context = useContext(NotificationContext);
  if (!context) {
    throw new Error('useNotification harus digunakan di dalam NotificationProvider');
  }
  return context;
};

// Provider component
export const NotificationProvider = ({ children }) => {
  const [notifications, setNotifications] = useState([]);

  // Fungsi untuk menambah notifikasi
  const showNotification = useCallback((message, type = 'info', duration = 3000) => {
    const id = Date.now(); // id unik sederhana
    setNotifications(prev => [...prev, { id, message, type }]);

    // Hapus notifikasi setelah duration
    setTimeout(() => {
      setNotifications(prev => prev.filter(n => n.id !== id));
    }, duration);
  }, []);

  // Fungsi untuk menghapus notifikasi secara manual (jika diperlukan)
  const removeNotification = useCallback((id) => {
    setNotifications(prev => prev.filter(n => n.id !== id));
  }, []);

  // Nilai yang akan disediakan ke consumer
  const value = {
    notifications,
    showNotification,
    removeNotification,
  };

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

Penjelasan:

  • notifications adalah array berisi objek notifikasi (id, message, type).
  • showNotification menambahkan notifikasi baru dan mengatur timer untuk menghapusnya.
  • removeNotification untuk menghapus secara manual (misal jika user mengklik close).
  • Kita menggunakan useCallback agar fungsi tidak berubah pada setiap render.

Langkah 2: Membuat Komponen Notifikasi (Toast)

Buat komponen untuk menampilkan notifikasi. Komponen ini akan ditempatkan di root aplikasi (misal di App.jsx) sehingga bisa muncul di atas halaman mana pun. Buat file src/components/Notification.jsx.

import React from 'react';
import { useNotification } from '../contexts/NotificationContext';
import './Notification.css';

const Notification = () => {
  const { notifications, removeNotification } = useNotification();

  if (notifications.length === 0) return null;

  return (
    <div className="notification-container">
      {notifications.map(notif => (
        <div
          key={notif.id}
          className={`notification notification-${notif.type}`}
          onClick={() => removeNotification(notif.id)}
        >
          <span>{notif.message}</span>
          <button className="close-btn">×</button>
        </div>
      ))}
    </div>
  );
};

export default Notification;

Buat file CSS src/components/Notification.css:

.notification-container {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.notification {
  min-width: 250px;
  padding: 15px 20px;
  border-radius: 4px;
  color: white;
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
  cursor: pointer;
  animation: slideIn 0.3s ease;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.notification-info {
  background-color: #3498db;
}

.notification-success {
  background-color: #27ae60;
}

.notification-error {
  background-color: #e74c3c;
}

.notification-warning {
  background-color: #f39c12;
}

.close-btn {
  background: none;
  border: none;
  color: white;
  font-size: 1.2em;
  cursor: pointer;
  padding: 0 0 0 10px;
}

Langkah 3: Membuat AuthContext (Opsional)

Kita juga bisa membuat context untuk data user yang login. Ini akan memudahkan akses ke informasi user di komponen mana pun tanpa perlu mengambil dari localStorage setiap saat. Buat file src/contexts/AuthContext.jsx.

import React, { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth harus digunakan di dalam AuthProvider');
  }
  return context;
};

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Saat aplikasi dimuat, cek localStorage
    const token = localStorage.getItem('token');
    const storedUser = localStorage.getItem('user');
    if (token && storedUser) {
      setUser(JSON.parse(storedUser));
    }
    setLoading(false);
  }, []);

  const login = (token, userData) => {
    localStorage.setItem('token', token);
    localStorage.setItem('user', JSON.stringify(userData));
    setUser(userData);
  };

  const logout = () => {
    localStorage.removeItem('token');
    localStorage.removeItem('user');
    setUser(null);
  };

  const value = {
    user,
    loading,
    login,
    logout,
  };

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

Dengan AuthContext, komponen bisa mengakses user dan fungsi login/logout dengan mudah. Kita juga bisa mengganti mekanisme login di halaman Login untuk menggunakan context ini.

⚙Langkah 4: Menggabungkan Provider di Aplikasi

Buka src/main.jsx (atau App.jsx tergantung struktur) dan bungkus aplikasi dengan provider-provider yang sudah dibuat. Pastikan urutannya benar.

import React from 'react';
import ReactDOM from 'react-dom/client';
import { NotificationProvider } from './contexts/NotificationContext';
import { AuthProvider } from './contexts/AuthContext';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <AuthProvider>
      <NotificationProvider>
        <App />
      </NotificationProvider>
    </AuthProvider>
  </React.StrictMode>
);

Kemudian di App.jsx, kita tambahkan komponen Notification di suatu tempat agar muncul di seluruh halaman. Misalnya, taruh di dalam layout utama atau langsung di App setelah router.

import { RouterProvider } from 'react-router-dom';
import router from './router'; // asumsikan router di file terpisah
import Notification from './components/Notification';

function App() {
  return (
    <>
      <Notification />
      <RouterProvider router={router} />
    </>
  );
}

export default App;

Langkah 5: Menggunakan Notifikasi di Komponen

Sekarang kita bisa memanggil notifikasi dari mana saja. Misalnya, di halaman AdminDestinations.jsx, setelah berhasil menambah atau menghapus data, kita tampilkan notifikasi sukses.

Modifikasi AdminDestinations.jsx:

import { useNotification } from '../../contexts/NotificationContext';

// di dalam komponen
const { showNotification } = useNotification();

const handleDelete = async (id) => {
  if (!window.confirm('Yakin ingin menghapus?')) return;
  try {
    await deleteDestination(id);
    setDestinations(destinations.filter(d => d.id !== id));
    showNotification('Destinasi berhasil dihapus', 'success');
  } catch (err) {
    showNotification('Gagal menghapus destinasi', 'error');
  }
};

Pada form tambah/edit, setelah sukses simpan, kita bisa redirect dan tampilkan notifikasi.

const handleSubmit = async (e) => {
  e.preventDefault();
  // ... proses
  try {
    if (id) {
      await updateDestination(id, data);
      showNotification('Destinasi berhasil diperbarui', 'success');
    } else {
      await createDestination(data);
      showNotification('Destinasi berhasil ditambahkan', 'success');
    }
    navigate('/admin/destinations');
  } catch (err) {
    showNotification('Gagal menyimpan data', 'error');
  }
};

Langkah 6: Menggunakan AuthContext di Halaman Login

Ubah halaman Login.jsx untuk menggunakan AuthContext alih-alih langsung menyimpan ke localStorage.

import { useAuth } from '../contexts/AuthContext';
import { useNotification } from '../contexts/NotificationContext';

const Login = () => {
  const { login } = useAuth();
  const { showNotification } = useNotification();
  const navigate = useNavigate();
  // ... state email, password

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await API.post('/auth/login', { email, password });
      const { token, user } = response.data;
      login(token, user);
      showNotification('Login berhasil', 'success');
      navigate('/admin/destinations');
    } catch (err) {
      showNotification('Email atau password salah', 'error');
    }
  };
  // ...
};

Di komponen lain, misalnya di AdminLayout, kita bisa gunakan useAuth untuk logout:

import { useAuth } from '../contexts/AuthContext';

const AdminLayout = () => {
  const { logout, user } = useAuth();
  const navigate = useNavigate();

  const handleLogout = () => {
    logout();
    navigate('/login');
  };

  return (
    <div className="admin-layout">
      <aside className="sidebar">
        <h3>Admin Panel</h3>
        <p>Halo, {user?.username}</p>
        {/* ... */}
        <button onClick={handleLogout}>Logout</button>
      </aside>
      {/* ... */}
    </div>
  );
};

Penjelasan Tambahan

  • Mengapa menggunakan Context API? – Untuk menghindari prop drilling dan memudahkan akses state di banyak komponen.
  • Kapan sebaiknya menggunakan Context? – Untuk state yang bersifat global seperti user, tema, notifikasi, keranjang belanja. Untuk state lokal komponen, tetap gunakan useState.
  • Performa: Setiap kali nilai context berubah, semua komponen yang menggunakan context akan re-render. Untuk menghindari re-render yang tidak perlu, kita bisa memisahkan context atau menggunakan useMemo.

Langkah Selanjutnya

Di Studi Kasus #22, kita akan menambahkan fitur untuk menampilkan galeri foto dengan lightbox (gambar tampil besar saat diklik) dan mungkin menambahkan fitur pemesanan sederhana jika diinginkan.

Pastikan notifikasi dan auth context berfungsi dengan baik di seluruh aplikasi. Coba simulasi error dan sukses untuk melihat notifikasi muncul.

Sampai jumpa di tutorial berikutnya!


Ditulis dengan ❤️ untuk para pengembang yang ingin membangun aplikasi wisata.

Lebih baru Lebih lama

نموذج الاتصال