Integrasi API: Axios, onMounted, & Rendering Data
"Menghubungkan Antarmuka dengan Realitas Data di Server"
1 Kenapa Menggunakan Axios?
Aplikasi Frontend modern tidak berdiri sendiri; mereka membutuhkan data dari server (Backend) untuk ditampilkan kepada pengguna. Untuk melakukan komunikasi ini, kita melakukan apa yang disebut HTTP Request (GET, POST, PUT, DELETE).
Secara bawaan, JavaScript modern memiliki fitur bernama fetch(). Namun,
komunitas Vue.js (dan React/Angular) sangat sering menggunakan pustaka pihak ketiga
bernama Axios. Mengapa?
Transformasi JSON Otomatis
Jika menggunakan
fetch, kita harus menulis response.json(). Axios
melakukan ini secara otomatis di balik layar.
Error Handling Lebih Baik
Axios otomatis melempar error (me-reject promise) jika status code bukan 2xx (misal 404 atau 500). Fetch tidak melakukan ini.
Interceptors & Security
Sangat mudah menambahkan token otorisasi (JWT) ke setiap request secara otomatis menggunakan fitur Interceptors.
Request Cancellation
Axios memungkinkan kita membatalkan request yang sedang berjalan (sangat berguna untuk fitur pencarian / auto-complete).
Untuk menginstal Axios di proyek Vue Anda, cukup jalankan perintah berikut di terminal:
npm install axios
2 Lifecycle: Kapan Harus Fetch Data?
Pertanyaan terbesar pemula adalah: "Di mana saya harus meletakkan kode untuk
mengambil data API?". Jawabannya adalah di dalam Hook onMounted.
onMounted adalah sebuah fungsi dari Vue yang menjamin bahwa komponen Anda
telah selesai dibuat dan dimasukkan ke dalam DOM browser. Ini adalah momen paling aman
untuk memanggil API eksternal.
Konsep 3 State (Tiga Keadaan)
Dalam melakukan HTTP Request, seorang Kader Engineer yang baik harus selalu menyiapkan 3 variabel state reaktif:
- Data State: Untuk menyimpan hasil (response) dari API. Biasanya
berupa Array kosong
ref([]). - Loading State: Bernilai boolean
ref(false). Diubah menjaditruesaat request berjalan, dan kembalifalsesaat selesai. - Error State: Untuk menyimpan pesan error jika API gagal diakses
ref(null).
Mari kita lihat struktur dasar kodenya:
<script setup>
import { ref, onMounted } from 'vue';
import axios from 'axios';
// 1. Deklarasi 3 State Utama
const users = ref([]); // Menyimpan daftar pengguna
const isLoading = ref(false); // Indikator loading UI
const errorMessage = ref(null); // Penampung pesan error
// 2. Buat fungsi Async untuk memanggil API
const fetchUsers = async () => {
isLoading.value = true; // Mulai loading
errorMessage.value = null; // Reset error sebelumnya
try {
// Menggunakan Axios GET Request
const response = await axios.get('https://jsonplaceholder.typicode.com/users');
// Axios menyimpan data asli di dalam properti .data
users.value = response.data;
} catch (error) {
// Menangkap error (misal internet putus atau server mati)
console.error("Gagal mengambil data:", error);
errorMessage.value = "Maaf, terjadi kesalahan saat memuat data.";
} finally {
// Blok ini selalu dijalankan baik sukses maupun error
isLoading.value = false; // Matikan loading
}
};
// 3. Panggil fungsi di onMounted
onMounted(() => {
fetchUsers();
});
</script>
3 Rendering List dengan v-for
Setelah data berhasil diambil dan dimasukkan ke dalam variabel users,
langkah selanjutnya adalah menampilkannya di HTML. Vue memiliki direktif khusus yang
sangat powerful bernama v-for.
Cara kerja v-for sangat mirip dengan perulangan for...of di
JavaScript. Anda menggunakan sintaks v-for="item in items".
Pentingnya Atribut :key
Ketika menggunakan v-for, Anda wajib menyertakan atribut
:key (shorthand dari v-bind:key). Key ini harus berupa nilai
unik (biasanya ID dari database).
Mengapa ini wajib? Vue menggunakan Virtual DOM. Jika urutan data berubah (misal karena fitur *sorting* atau *filtering*), Vue butuh penanda unik (key) untuk melacak elemen HTML mana yang harus dipindah, dihapus, atau dipertahankan tanpa harus merender ulang seluruh daftar dari awal. Ini membuat performa aplikasi Anda sangat cepat.
<template>
<div class="container">
<h2>Daftar Pengguna</h2>
<!-- 1. Tampilkan Loading State -->
<div v-if="isLoading" class="loading-spinner">
Memuat data dari server...
</div>
<!-- 2. Tampilkan Error State -->
<div v-else-if="errorMessage" class="error-alert">
{{ errorMessage }}
<button @click="fetchUsers">Coba Lagi</button>
</div>
<!-- 3. Tampilkan Data State dengan v-for -->
<ul v-else class="user-list">
<!-- :key wajib diisi dengan identifier unik! -->
<li
v-for="user in users"
:key="user.id"
class="user-card"
>
<h3>{{ user.name }}</h3>
<p>Email: {{ user.email }}</p>
<p>Perusahaan: {{ user.company.name }}</p>
</li>
</ul>
</div>
</template>
4. Implementasi Skala Produksi: Employee Directory
Untuk memberikan gambaran utuh, di bawah ini adalah file Vue Single File Component (SFC) lengkap. Kode ini tidak hanya melakukan fetch data, tetapi juga menyertakan:
- Skeleton Loading (UI Modern seperti Facebook/LinkedIn saat memuat data).
- Computed Properties untuk fitur Pencarian (Search).
- Styling lengkap menggunakan Tailwind CSS via attribut
class. - Pemetaan Data (Mapping).
<!--
========================================================================
KOMPONEN: EmployeeDirectory.vue
DESKRIPSI: Komponen tingkat lanjut untuk mengambil, menampilkan,
dan memfilter data pegawai menggunakan Axios & Vue 3.
AUTHOR: Catatan Kader
========================================================================
-->
<script setup>
import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
// ---------------------------------------------------------
// STATE MANAGEMENT (REAKTIVITAS)
// ---------------------------------------------------------
const employees = ref([]);
const isLoading = ref(true); // Set true di awal karena data langsung diambil saat mount
const errorMessage = ref(null);
// State untuk fitur pencarian
const searchQuery = ref('');
// ---------------------------------------------------------
// COMPUTED PROPERTIES (LOGIKA TURUNAN)
// ---------------------------------------------------------
/**
* filteredEmployees memastikan daftar yang tampil hanya yang
* sesuai dengan kata kunci pencarian pada nama atau email.
*/
const filteredEmployees = computed(() => {
// Jika query kosong, kembalikan semua data
if (!searchQuery.value) return employees.value;
const lowerCaseQuery = searchQuery.value.toLowerCase();
return employees.value.filter(emp =>
emp.name.toLowerCase().includes(lowerCaseQuery) ||
emp.email.toLowerCase().includes(lowerCaseQuery)
);
});
// ---------------------------------------------------------
// METHODS (FUNGSI UTAMA)
// ---------------------------------------------------------
/**
* fetchEmployees()
* Melakukan HTTP GET Request ke endpoint publik JSONPlaceholder.
* Menggunakan async/await dan try-catch untuk penanganan error.
*/
const fetchEmployees = async () => {
// Mulai loading state dan reset error
isLoading.value = true;
errorMessage.value = null;
try {
// Simulasi delay jaringan agar skeleton loading terlihat (Opsional)
await new Promise(resolve => setTimeout(resolve, 1500));
const API_URL = 'https://jsonplaceholder.typicode.com/users';
const response = await axios.get(API_URL);
// Memasukkan data dari response ke variabel reaktif
employees.value = response.data;
} catch (error) {
console.error('Axios Error:', error);
// Memberikan pesan error yang ramah pengguna berdasarkan tipe error
if (error.response) {
// Server merespon tapi dengan status error (4xx, 5xx)
errorMessage.value = `Gagal memuat data (Kode: ${error.response.status}). Server mengalami masalah.`;
} else if (error.request) {
// Request terkirim tapi tidak ada respon (Sinyal/Internet mati)
errorMessage.value = 'Tidak dapat terhubung ke server. Periksa koneksi internet Anda.';
} else {
// Kesalahan setup request
errorMessage.value = 'Terjadi kesalahan tidak terduga dalam sistem.';
}
} finally {
// Pastikan loading selalu dimatikan di akhir
isLoading.value = false;
}
};
// ---------------------------------------------------------
// LIFECYCLE HOOKS
// ---------------------------------------------------------
onMounted(() => {
// Eksekusi fetch segera setelah komponen dimasukkan ke DOM
fetchEmployees();
});
</script>
<template>
<div class="min-h-screen bg-slate-50 p-8 font-sans">
<div class="max-w-6xl mx-auto">
<!-- HEADER & SEARCH SECTION -->
<div class="flex flex-col md:flex-row justify-between items-center mb-10 gap-6">
<div>
<h1 class="text-3xl font-extrabold text-slate-900 tracking-tight">Direktori Pegawai</h1>
<p class="text-slate-500 mt-2">Manajemen data SDM terpusat.</p>
</div>
<!-- Input Pencarian -->
<div class="relative w-full md:w-96">
<span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<!-- SVG Search Icon -->
<svg class="h-5 w-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</span>
<input
v-model="searchQuery"
type="text"
class="block w-full pl-10 pr-3 py-3 border border-slate-300 rounded-xl leading-5 bg-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-shadow shadow-sm"
placeholder="Cari nama atau email..."
/>
</div>
</div>
<!-- CONDITIONAL RENDERING -->
<!-- STATE 1: LOADING (SKELETON UI) -->
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Mengulang skeleton 6 kali menggunakan v-for dengan angka -->
<div v-for="i in 6" :key="i" class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
<div class="flex items-center space-x-4 mb-4">
<div class="rounded-full bg-slate-200 animate-pulse h-14 w-14"></div>
<div class="flex-1 space-y-2 py-1">
<div class="h-4 bg-slate-200 animate-pulse rounded w-3/4"></div>
<div class="h-3 bg-slate-200 animate-pulse rounded w-1/2"></div>
</div>
</div>
<div class="space-y-3 mt-6">
<div class="h-3 bg-slate-200 animate-pulse rounded w-full"></div>
<div class="h-3 bg-slate-200 animate-pulse rounded w-5/6"></div>
</div>
</div>
</div>
<!-- STATE 2: ERROR -->
<div v-else-if="errorMessage" class="rounded-xl bg-red-50 p-6 border border-red-200 flex flex-col items-center justify-center text-center py-12">
<svg class="h-12 w-12 text-red-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<h3 class="text-lg font-bold text-red-800 mb-2">Sistem Gagal Memuat Data</h3>
<p class="text-red-600 mb-6">{{ errorMessage }}</p>
<button @click="fetchEmployees" class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors shadow-sm">
Coba Muat Ulang
</button>
</div>
<!-- STATE 3: SUCCESS (Menampilkan Data) -->
<div v-else>
<!-- Jika pencarian tidak menemukan hasil -->
<div v-if="filteredEmployees.length === 0" class="text-center py-20">
<img src="https://cdn-icons-png.flaticon.com/512/7486/7486744.png" alt="Not found" class="w-32 h-32 mx-auto opacity-50 mb-4">
<p class="text-slate-500 text-lg">Tidak ada pegawai yang cocok dengan "<span class="font-bold text-slate-800">{{ searchQuery }}</span>"</p>
<button @click="searchQuery = ''" class="mt-4 text-blue-600 hover:text-blue-800 font-medium underline">Bersihkan pencarian</button>
</div>
<!-- Grid Rendering dengan v-for -->
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!--
Penggunaan v-for di sini
Kita me-looping filteredEmployees, bukan employees
sehingga filter search langsung berjalan otomatis.
:key diisi dengan id unik dari API.
-->
<div
v-for="employee in filteredEmployees"
:key="employee.id"
class="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm hover:shadow-lg hover:-translate-y-1 transition-all duration-300 group"
>
<!-- Bagian Avatar dan Nama -->
<div class="flex items-center space-x-4 mb-5">
<!-- Avatar generik menggunakan inisial nama -->
<div class="h-14 w-14 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold text-xl group-hover:bg-blue-600 group-hover:text-white transition-colors">
{{ employee.name.charAt(0) }}
</div>
<div>
<h3 class="text-lg font-bold text-slate-900 group-hover:text-blue-600 transition-colors">{{ employee.name }}</h3>
<p class="text-sm text-slate-500 font-mono">@{{ employee.username }}</p>
</div>
</div>
<!-- Divider -->
<hr class="border-slate-100 mb-4">
<!-- Informasi Detail -->
<div class="space-y-3 text-sm text-slate-600">
<!-- Email -->
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
<a :href="`mailto:${employee.email}`" class="hover:text-blue-600 hover:underline truncate">{{ employee.email }}</a>
</div>
<!-- Telepon -->
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
</svg>
<span>{{ employee.phone.split(' ')[0] }}</span>
</div>
<!-- Perusahaan -->
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
<span class="font-semibold text-slate-700">{{ employee.company.name }}</span>
</div>
</div>
</div>
</div> <!-- End Grid -->
</div> <!-- End Success State -->
</div>
</div>
</template>