Membangun UI Responsif dengan Concurrent React: Menguasai useTransition, useDeferredValue, dan Suspense
Pernahkah Anda merasa frustrasi saat aplikasi web yang Anda bangun terasa lambat atau “freeze” sebentar ketika ada banyak data yang diproses atau saat navigasi antar halaman? Rasanya seperti UI tiba-tiba macet, tidak responsif terhadap input pengguna, dan pengalaman pengguna pun jadi terganggu. Ini adalah masalah umum dalam pengembangan web, terutama dengan aplikasi yang kaya fitur dan data.
Sebagai developer, kita selalu mencari cara untuk membuat aplikasi lebih cepat dan lebih responsif. Di sinilah Concurrent React hadir sebagai game-changer. Concurrent React adalah fitur fundamental dari React yang memungkinkan aplikasi Anda tetap responsif bahkan saat melakukan tugas-tugas berat di latar belakang. Ini bukan tentang membuat kode Anda berjalan lebih cepat secara absolut, melainkan tentang bagaimana React dapat mengelola prioritas dan menunda pekerjaan yang tidak mendesak agar UI tetap interaktif.
Dalam artikel ini, kita akan menyelami dunia Concurrent React dan tiga pilar utamanya yang akan membantu Anda membangun UI yang jauh lebih mulus: useTransition, useDeferredValue, dan Suspense. Mari kita mulai!
1. Memahami Masalah: Blocking Renders di React Tradisional
Secara default, React bekerja secara sinkron. Artinya, ketika Anda mengupdate state, React akan segera mencoba merender ulang seluruh komponen yang terpengaruh. Jika update state tersebut memicu banyak perhitungan atau rendering komponen yang kompleks, seluruh thread utama browser bisa terblokir.
Skenario Umum:
- Input Pencarian yang Lambat: Anda mengetik di kolom pencarian, dan setiap karakter yang Anda ketik memicu filter data yang besar. Jika filter ini memakan waktu, input Anda akan terasa laggy dan tidak responsif.
- Navigasi yang Terasa “Stuck”: Anda mengklik tombol navigasi yang memuat banyak komponen baru atau melakukan fetching data. Selama proses loading, tombol mungkin tidak memberikan feedback visual, atau seluruh halaman terasa beku.
- Animasi yang Tidak Mulus: Animasi yang seharusnya halus menjadi patah-patah karena ada render lain yang memblokir.
📌 Masalah utamanya adalah React tidak bisa “menginterupsi” pekerjaan yang sedang berjalan untuk merespons input pengguna yang lebih penting.
2. Apa Itu Concurrent React?
Concurrent React adalah arsitektur baru di React yang memungkinkan React untuk menghentikan, menunda, atau melanjutkan pekerjaan rendering berdasarkan prioritas. Ini seperti memiliki “otak” tambahan yang bisa memutuskan pekerjaan mana yang lebih penting untuk dilakukan sekarang, dan mana yang bisa menunggu.
Konsep Kunci:
- Interruption: React bisa berhenti merender jika ada input pengguna yang lebih penting (misalnya, mengetik di input).
- Prioritization: React membedakan antara urgent updates (seperti mengetik, mengklik) dan non-urgent updates (seperti menampilkan hasil pencarian yang difilter, transisi halaman).
- Background Rendering: Pekerjaan non-urgent bisa dilakukan di latar belakang tanpa memblokir UI utama.
✅ Manfaatnya: UI aplikasi Anda akan terasa lebih responsif, cepat, dan memberikan pengalaman pengguna yang lebih baik, bahkan pada perangkat yang kurang bertenaga atau dengan data yang kompleks.
3. useTransition: Menjaga UI Tetap Responsif Selama Transisi
useTransition adalah hook yang memungkinkan Anda menandai pembaruan state sebagai “transisi” (non-urgent). Ini memberitahu React bahwa update tersebut bisa ditunda jika ada pekerjaan yang lebih mendesak.
🎯 Kapan Menggunakan useTransition?
Ketika Anda memiliki update state yang memicu perubahan UI yang signifikan atau perhitungan yang berat, tetapi Anda tidak ingin update tersebut memblokir interaksi pengguna lainnya. Contoh paling umum adalah filtering data, navigasi, atau sorting.
Bagaimana Cara Kerjanya?
useTransition mengembalikan array dengan dua elemen:
isPending: Sebuah boolean yang menunjukkan apakah transisi sedang berlangsung. Berguna untuk menampilkan indikator loading.startTransition: Sebuah fungsi yang Anda gunakan untuk membungkus update state yang ingin Anda tandai sebagai transisi.
Contoh Konkret: Input Pencarian yang Responsif
Misalkan Anda memiliki input pencarian yang memfilter daftar panjang. Tanpa useTransition, setiap kali Anda mengetik, UI mungkin terasa lambat.
import React, { useState, useTransition } from 'react';
const generateBigList = (size) => {
const list = [];
for (let i = 0; i < size; i++) {
list.push(`Item ${i + 1}`);
}
return list;
};
const items = generateBigList(10000); // Daftar yang sangat panjang
function SearchableList() {
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [isPending, startTransition] = useTransition(); // 👈 useTransition di sini
const handleChange = (e) => {
setInputValue(e.target.value); // Ini adalah urgent update (langsung responsif)
// Bungkus update searchQuery dengan startTransition
startTransition(() => {
setSearchQuery(e.target.value); // Ini adalah non-urgent update
});
};
const filteredItems = items.filter(item =>
item.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="Cari item..."
style={{ width: '300px', padding: '10px', fontSize: '16px' }}
/>
{isPending && <p style={{ color: 'gray' }}>Sedang mencari...</p>} {/* 👈 Indikator loading */}
<ul style={{ maxHeight: '300px', overflowY: 'auto', border: '1px solid #eee', marginTop: '10px' }}>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default SearchableList;
Dalam contoh ini:
setInputValue(untuk input field) adalah urgent update. Setiap ketikan akan langsung memperbarui nilai input.setSearchQuery(untuk memfilter daftar) adalah non-urgent update. React akan melakukan filter di latar belakang. Jika Anda mengetik dengan cepat, React akan memprioritaskan update input field dan menunda/menginterupsi proses filtering yang sedang berjalan. Hasilnya, input field akan selalu responsif.
4. useDeferredValue: Menunda Update UI yang Berat
useDeferredValue mirip dengan useTransition, tetapi digunakan untuk menunda rendering nilai di dalam komponen, bukan membungkus update state. Ini sangat berguna ketika Anda memiliki nilai yang membutuhkan waktu untuk dirender, dan Anda ingin React menunda rendering nilai tersebut sampai thread utama browser bebas.
🎯 Kapan Menggunakan useDeferredValue?
Ketika Anda ingin menunjukkan versi “lama” dari UI sementara versi “baru” yang berat sedang dihitung di latar belakang. Ini seperti debounce yang dioptimalkan oleh React scheduler.
Bagaimana Cara Kerjanya?
useDeferredValue(value) akan mengembalikan versi “tertunda” dari value. React akan mencoba memperbarui nilai tertunda ini ketika ada waktu luang, tanpa memblokir UI.
Contoh Konkret: Menampilkan Hasil Pencarian yang Difilter (lanjutan dari useTransition)
Mari kita gunakan kembali contoh SearchableList dan terapkan useDeferredValue untuk daftar hasil.
import React, { useState, useTransition, useDeferredValue } from 'react';
const generateBigList = (size) => {
const list = [];
for (let i = 0; i < size; i++) {
list.push(`Item ${i + 1}`);
}
return list;
};
const items = generateBigList(10000);
function SearchableListDeferred() {
const [inputValue, setInputValue] = useState('');
const [isPending, startTransition] = useTransition();
const deferredInputValue = useDeferredValue(inputValue); // 👈 Nilai input yang ditunda
// Menandai apakah deferredInputValue masih "lama" atau sedang menunggu update
const isDeferredPending = inputValue !== deferredInputValue;
const handleChange = (e) => {
setInputValue(e.target.value);
};
// Gunakan deferredInputValue untuk filter, bukan langsung inputValue
const filteredItems = items.filter(item =>
item.toLowerCase().includes(deferredInputValue.toLowerCase())
);
return (
<div>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="Cari item..."
style={{ width: '300px', padding: '10px', fontSize: '16px' }}
/>
{/* Indikator loading untuk deferred value */}
{isDeferredPending && <p style={{ color: 'gray' }}>Sedang memperbarui hasil...</p>}
<ul style={{ maxHeight: '300px', overflowY: 'auto', border: '1px solid #eee', marginTop: '10px' }}>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
export default SearchableListDeferred;
Perbedaan utama dengan useTransition:
useTransitionmembungkus update state itu sendiri.useDeferredValuemembungkus nilai yang akan digunakan untuk rendering.
Dalam contoh ini, inputValue akan segera diupdate, tetapi deferredInputValue akan menunggu sampai React memiliki waktu luang untuk memperbarui hasil filter. Ini memastikan input field tetap responsif, dan daftar hasil akan diperbarui secara mulus di latar belakang.
5. Suspense: Menangani Data Fetching dan Code Splitting dengan Elegan
Suspense adalah fitur React yang memungkinkan komponen “menunggu” sesuatu (seperti data, kode, atau gambar) sebelum dirender. Ini memberikan cara yang deklaratif untuk mengelola status loading, bukan dengan isLoading boolean yang seringkali membuat kode menjadi berantakan.
🎯 Kapan Menggunakan Suspense?
Untuk menampilkan UI fallback (misalnya, spinner loading) saat komponen atau data yang dibutuhkan belum siap. Paling sering digunakan untuk code splitting (React.lazy) dan data fetching.
Contoh Konkret: Code Splitting dengan React.lazy
Ini adalah penggunaan Suspense yang paling umum dan sudah ada sejak lama.
import React, { Suspense } from 'react';
// Lazy load komponen DetailProduk
const DetailProduk = React.lazy(() => import('./DetailProduk'));
function App() {
const [showDetails, setShowDetails] = useState(false);
return (
<div>
<h1>Aplikasi Toko Online</h1>
<button onClick={() => setShowDetails(!showDetails)}>
{showDetails ? 'Sembunyikan' : 'Tampilkan'} Detail Produk
</button>
{showDetails && (
<Suspense fallback={<div>Loading Detail Produk...</div>}> {/* 👈 Suspense */}
<DetailProduk />
</Suspense>
)}
</div>
);
}
// Komponen DetailProduk.js (akan di-lazy load)
// function DetailProduk() {
// return (
// <div>
// <h2>Detail Produk Spesial</h2>
// <p>Ini adalah deskripsi produk yang dimuat secara dinamis.</p>
// </div>
// );
// }
Ketika showDetails menjadi true, React akan mencoba memuat komponen DetailProduk. Selama proses pemuatan, fallback dari Suspense akan ditampilkan. Setelah DetailProduk selesai dimuat, ia akan menggantikan fallback.
Suspense untuk Data Fetching (dengan Library)
Meskipun React belum menyediakan solusi built-in untuk data fetching dengan Suspense secara langsung (Anda tidak bisa hanya await Promise di komponen), library seperti React Query (TanStack Query) atau SWR telah mengintegrasikan dukungan Suspense.
Konsepnya:
- Anda mengkonfigurasi
Suspensedi atas komponen yang melakukan fetching data. - Komponen tersebut akan “melempar” promise saat data belum siap.
- Suspense akan menangkap promise tersebut dan menampilkan
fallbackUI. - Setelah promise selesai (data fetched), komponen akan dirender dengan data.
// Contoh pseudo-code dengan React Query (membutuhkan konfigurasi Suspense di QueryClientProvider)
import React, { Suspense } from 'react';
import { useQuery } from '@tanstack/react-query';
function fetchUserData() {
return new Promise(resolve => setTimeout(() => resolve({ name: 'Budi', email: 'budi@example.com' }), 2000));
}
function UserProfile() {
// useQuery akan "melempar" promise jika data belum ada dan Suspense diaktifkan
const { data: user } = useQuery({ queryKey: ['user'], queryFn: fetchUserData, suspense: true });
return (
<div>
<h2>Profil Pengguna</h2>
<p>Nama: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
}
function AppWithSuspense() {
return (
<Suspense fallback={<div>Memuat profil pengguna...</div>}>
<UserProfile />
</Suspense>
);
}
export default AppWithSuspense;
⚠️ Penting: Suspense untuk data fetching membutuhkan setup yang benar di sisi library data fetching Anda dan juga bagaimana Anda mengelola Error Boundaries untuk menangani kegagalan fetching data.
6. Best Practices dan Kapan Menggunakan Masing-Masing
Memahami kapan dan bagaimana menggunakan useTransition, useDeferredValue, dan Suspense adalah kunci untuk membangun aplikasi yang optimal.
| Fitur | Kegunaan Utama | Kapan Digunakan |
|---|---|---|
useTransition | Menandai update state sebagai non-urgent, menjaga UI tetap responsif. | Untuk update state yang memicu rendering berat (filtering, sorting, navigasi) di mana Anda ingin input pengguna tetap direspons. |
useDeferredValue | Menunda rendering nilai mahal di dalam komponen, menunjukkan versi lama sementara. | Ketika Anda memiliki nilai yang membutuhkan waktu untuk dirender, dan Anda ingin menampilkan versi lama sampai versi baru siap. |
Suspense | Menampilkan UI fallback saat komponen atau data belum siap. | Untuk code splitting (React.lazy) atau data fetching (dengan library yang mendukung Suspense). |
Tips Tambahan:
- Jangan Over-Engineer: Tidak semua update state atau rendering memerlukan Concurrent React. Terapkan fitur ini pada bottleneck performa yang nyata yang mempengaruhi pengalaman pengguna.
- Error Boundaries: Selalu gunakan Error Boundaries di sekitar komponen yang menggunakan Suspense, terutama untuk data fetching. Ini akan menangani error fetching data dengan elegan.
- Kombinasi:
useTransitiondanuseDeferredValueseringkali digunakan bersama.useTransitionuntuk memperbarui state danuseDeferredValueuntuk menunda tampilan berdasarkan state tersebut. - Indikator Loading: Selalu berikan indikator loading (misalnya, spinner atau teks “Sedang mencari…”) saat
isPendingbernilaitrueatau ketika Suspense menampilkan fallback. Ini memberi feedback penting kepada pengguna.
Kesimpulan
Concurrent React, dengan useTransition, useDeferredValue, dan Suspense, adalah langkah maju yang signifikan dalam cara kita membangun aplikasi web. Ini memungkinkan kita untuk merancang UI yang secara inheren lebih responsif dan mulus, bahkan ketika berhadapan dengan data besar atau operasi rendering yang kompleks.
Dengan menguasai alat-alat ini, Anda tidak hanya meningkatkan performa teknis aplikasi Anda, tetapi juga secara drastis meningkatkan pengalaman pengguna. Jadi, lain kali Anda menghadapi masalah UI yang lambat atau tidak responsif, ingatlah Concurrent React dan berikan aplikasi Anda “superpower” yang layak didapatkan!
🔗 Baca Juga
- React Server Components (RSC): Revolusi Rendering di Aplikasi React Modern
- Menggali Lebih Dalam React Hooks: Panduan Praktis untuk Developer Modern
- Membangun Custom Hooks yang Kuat dan Reusable: Mengoptimalkan Logika dan State di Aplikasi React Anda
- Optimasi Data Fetching di Frontend: Menggali Lebih Dalam React Query (TanStack Query) dan SWR