Jelajahi teknik lanjutan untuk pengambilan data paralel di React menggunakan Suspense, tingkatkan kinerja aplikasi dan pengalaman pengguna. Pelajari strategi koordinasi operasi asinkron.
Koordinasi React Suspense: Menguasai Pengambilan Data Paralel
React Suspense telah merevolusi cara kita menangani operasi asinkron, khususnya pengambilan data. Ini memungkinkan komponen untuk "menangguhkan" rendering saat menunggu data dimuat, menyediakan cara deklaratif untuk mengelola status pemuatan. Namun, hanya membungkus pengambilan data individual dengan Suspense dapat menyebabkan efek air terjun, di mana satu pengambilan selesai sebelum yang berikutnya dimulai, yang berdampak negatif pada kinerja. Postingan blog ini membahas strategi lanjutan untuk mengoordinasikan beberapa pengambilan data secara paralel menggunakan Suspense, mengoptimalkan responsivitas aplikasi Anda, dan meningkatkan pengalaman pengguna untuk audiens global.
Memahami Masalah Air Terjun dalam Pengambilan Data
Bayangkan sebuah skenario di mana Anda perlu menampilkan profil pengguna dengan nama, avatar, dan aktivitas terbaru mereka. Jika Anda mengambil setiap bagian data secara berurutan, pengguna melihat pemutar pemuatan untuk nama, kemudian yang lain untuk avatar, dan akhirnya, satu untuk umpan aktivitas. Pola pemuatan berurutan ini menciptakan efek air terjun, menunda rendering profil lengkap dan membuat frustrasi pengguna. Bagi pengguna internasional dengan kecepatan jaringan yang bervariasi, penundaan ini bisa menjadi lebih terasa.
Pertimbangkan cuplikan kode yang disederhanakan ini:
function UserProfile() {
const name = useName(); // Mengambil nama pengguna
const avatar = useAvatar(name); // Mengambil avatar berdasarkan nama
const activity = useActivity(name); // Mengambil aktivitas berdasarkan nama
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="User Avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
Dalam contoh ini, useAvatar dan useActivity bergantung pada hasil dari useName. Ini menciptakan air terjun yang jelas – useAvatar dan useActivity tidak dapat mulai mengambil data sampai useName selesai. Ini tidak efisien dan merupakan hambatan kinerja yang umum.
Strategi untuk Pengambilan Data Paralel dengan Suspense
Kunci untuk mengoptimalkan pengambilan data dengan Suspense adalah dengan memulai semua permintaan data secara bersamaan. Berikut adalah beberapa strategi yang dapat Anda gunakan:
1. Memuat Data Sebelumnya dengan `React.preload` dan Sumber Daya
Salah satu teknik paling ampuh adalah memuat data sebelumnya sebelum komponen dirender. Ini melibatkan pembuatan "sumber daya" (objek yang merangkum janji pengambilan data) dan melakukan pra-pengambilan data. `React.preload` membantu dalam hal ini. Pada saat komponen membutuhkan data, data tersebut sudah tersedia, sehingga hampir seluruhnya menghilangkan status pemuatan.
Pertimbangkan sumber daya untuk mengambil produk:
const createProductResource = (productId) => {
let promise;
let product;
let error;
const suspender = new Promise((resolve, reject) => {
promise = fetch(`/api/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
product = data;
resolve();
})
.catch(e => {
error = e;
reject(e);
});
});
return {
read() {
if (error) {
throw error;
}
if (product) {
return product;
}
throw suspender;
},
};
};
// Penggunaan:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
Sekarang, Anda dapat memuat sumber daya ini sebelumnya sebelum komponen ProductDetails dirender. Misalnya, selama transisi rute atau saat mengarahkan kursor.
React.preload(productResource);
Ini memastikan bahwa data kemungkinan besar tersedia pada saat komponen ProductDetails membutuhkannya, meminimalkan atau menghilangkan status pemuatan.
2. Menggunakan `Promise.all` untuk Pengambilan Data Bersamaan
Pendekatan sederhana dan efektif lainnya adalah dengan menggunakan Promise.all untuk memulai semua pengambilan data secara bersamaan dalam satu batas Suspense. Ini berfungsi dengan baik ketika dependensi data diketahui di muka.
Mari kita tinjau kembali contoh profil pengguna. Alih-alih mengambil data secara berurutan, kita dapat mengambil nama, avatar, dan umpan aktivitas secara bersamaan:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simulasikan panggilan API
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simulasikan panggilan API
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simulasikan panggilan API
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
function Name() {
const name = useSuspense(fetchName());
return <h2>{name}</h2>;
}
function Avatar({ name }) {
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity({ name }) {
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const name = useSuspense(fetchName());
return (
<div>
<Suspense fallback=<div>Loading Avatar...</div>>
<Avatar name={name} />
</Suspense>
<Suspense fallback=<div>Loading Activity...</div>>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
Namun, jika setiap `Avatar` dan `Activity` juga bergantung pada `fetchName`, tetapi dirender di dalam batas suspense terpisah, Anda dapat mengangkat janji `fetchName` ke induk dan menyediakannya melalui Konteks React.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simulasikan panggilan API
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simulasikan panggilan API
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simulasikan panggilan API
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
const NamePromiseContext = createContext(null);
function Avatar() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const namePromise = fetchName();
return (
<NamePromiseContext.Provider value={namePromise}>
<Suspense fallback=<div>Loading Avatar...</div>>
<Avatar />
</Suspense>
<Suspense fallback=<div>Loading Activity...</div>>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. Menggunakan Hook Kustom untuk Mengelola Pengambilan Paralel
Untuk skenario yang lebih kompleks dengan potensi dependensi data kondisional, Anda dapat membuat hook kustom untuk mengelola pengambilan data paralel dan mengembalikan sumber daya yang dapat digunakan Suspense.
import { useState, useEffect, useRef } from 'react';
function useParallelData(fetchFunctions) {
const [resource, setResource] = useState(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
const promises = fetchFunctions.map(fn => fn());
const suspender = Promise.all(promises).then(
(results) => {
if (mounted.current) {
setResource({ status: 'success', value: results });
}
},
(error) => {
if (mounted.current) {
setResource({ status: 'error', value: error });
}
}
);
setResource({
status: 'pending',
value: suspender,
});
return () => {
mounted.current = false;
};
}, [fetchFunctions]);
const read = () => {
if (!resource) {
throw new Error('Resource not yet initialized');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// Contoh penggunaan:
async function fetchUserData(userId) {
// Simulasikan panggilan API
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'User ' + userId };
}
async function fetchUserPosts(userId) {
// Simulasikan panggilan API
await new Promise(resolve => setTimeout(resolve, 500));
return [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }];
}
function UserProfile({ userId }) {
const { read } = useParallelData([
() => fetchUserData(userId),
() => fetchUserPosts(userId),
]);
const [userData, userPosts] = read();
return (
<div>
<h2>{userData.name}</h2>
<ul>
{userPosts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback=<div>Loading user data...</div>>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
Pendekatan ini merangkum kompleksitas pengelolaan janji dan status pemuatan di dalam hook, membuat kode komponen lebih bersih dan lebih fokus pada rendering data.
4. Hidrasi Selektif dengan Streaming Server Rendering
Untuk aplikasi yang dirender server, React 18 memperkenalkan hidrasi selektif dengan streaming server rendering. Ini memungkinkan Anda untuk mengirim HTML ke klien dalam potongan-potongan saat tersedia di server. Anda dapat membungkus komponen yang lambat dimuat dengan batas <Suspense>, memungkinkan bagian halaman lainnya menjadi interaktif sementara komponen yang lambat masih dimuat di server. Ini secara dramatis meningkatkan kinerja yang dirasakan, terutama bagi pengguna dengan koneksi atau perangkat jaringan yang lambat.
Pertimbangkan skenario di mana situs web berita perlu menampilkan artikel dari berbagai wilayah di dunia (misalnya, Asia, Eropa, Amerika). Beberapa sumber data mungkin lebih lambat daripada yang lain. Hidrasi selektif memungkinkan menampilkan artikel dari wilayah yang lebih cepat terlebih dahulu, sementara artikel dari wilayah yang lebih lambat masih dimuat, mencegah seluruh halaman diblokir.
Menangani Kesalahan dan Status Pemuatan
Meskipun Suspense menyederhanakan manajemen status pemuatan, penanganan kesalahan tetap penting. Batas kesalahan (menggunakan metode siklus hidup componentDidCatch atau hook useErrorBoundary dari pustaka seperti `react-error-boundary`) memungkinkan Anda untuk menangani kesalahan yang terjadi selama pengambilan data atau rendering dengan baik. Batas kesalahan ini harus ditempatkan secara strategis untuk menangkap kesalahan dalam batas Suspense tertentu, mencegah seluruh aplikasi mogok.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... mengambil data yang mungkin salah
}
function App() {
return (
<ErrorBoundary fallback={<div>Something went wrong!</div>}>
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
Ingatlah untuk menyediakan UI fallback yang informatif dan ramah pengguna untuk status pemuatan dan kesalahan. Ini sangat penting bagi pengguna internasional yang mungkin mengalami kecepatan jaringan yang lebih lambat atau pemadaman layanan regional.
Praktik Terbaik untuk Mengoptimalkan Pengambilan Data dengan Suspense
- Identifikasi dan Prioritaskan Data Kritis: Tentukan data mana yang penting untuk rendering awal aplikasi Anda dan prioritaskan pengambilan data tersebut terlebih dahulu.
- Muat Data Sebelumnya Jika Memungkinkan: Gunakan `React.preload` dan sumber daya untuk memuat data sebelumnya sebelum komponen membutuhkannya, meminimalkan status pemuatan.
- Ambil Data Secara Bersamaan: Manfaatkan `Promise.all` atau hook kustom untuk memulai beberapa pengambilan data secara paralel.
- Optimalkan Titik Akhir API: Pastikan titik akhir API Anda dioptimalkan untuk kinerja, meminimalkan latensi dan ukuran muatan. Pertimbangkan untuk menggunakan teknik seperti GraphQL untuk mengambil hanya data yang Anda butuhkan.
- Terapkan Penembolokan: Cache data yang sering diakses untuk mengurangi jumlah permintaan API. Pertimbangkan untuk menggunakan pustaka seperti `swr` atau `react-query` untuk kemampuan penembolokan yang kuat.
- Gunakan Pemisahan Kode: Pisahkan aplikasi Anda menjadi potongan-potongan yang lebih kecil untuk mengurangi waktu muat awal. Gabungkan pemisahan kode dengan Suspense untuk memuat dan merender berbagai bagian aplikasi Anda secara progresif.
- Pantau Kinerja: Pantau kinerja aplikasi Anda secara teratur menggunakan alat seperti Lighthouse atau WebPageTest untuk mengidentifikasi dan mengatasi hambatan kinerja.
- Tangani Kesalahan dengan Baik: Terapkan batas kesalahan untuk menangkap kesalahan selama pengambilan data dan rendering, memberikan pesan kesalahan yang informatif kepada pengguna.
- Pertimbangkan Server-Side Rendering (SSR): Untuk alasan SEO dan kinerja, pertimbangkan untuk menggunakan SSR dengan streaming dan hidrasi selektif untuk memberikan pengalaman awal yang lebih cepat.
Kesimpulan
React Suspense, ketika dikombinasikan dengan strategi untuk pengambilan data paralel, menyediakan toolkit yang ampuh untuk membangun aplikasi web yang responsif dan berkinerja tinggi. Dengan memahami masalah air terjun dan menerapkan teknik seperti pemuatan sebelumnya, pengambilan serentak dengan Promise.all, dan hook kustom, Anda dapat secara signifikan meningkatkan pengalaman pengguna. Ingatlah untuk menangani kesalahan dengan baik dan memantau kinerja untuk memastikan aplikasi Anda tetap dioptimalkan untuk pengguna di seluruh dunia. Seiring React terus berkembang, menjelajahi fitur-fitur baru seperti hidrasi selektif dengan streaming server rendering akan semakin meningkatkan kemampuan Anda untuk memberikan pengalaman pengguna yang luar biasa, terlepas dari lokasi atau kondisi jaringan. Dengan menerapkan teknik-teknik ini, Anda dapat membuat aplikasi yang tidak hanya fungsional tetapi juga menyenangkan untuk digunakan bagi audiens global Anda.
Postingan blog ini bertujuan untuk memberikan ikhtisar komprehensif tentang strategi pengambilan data paralel dengan React Suspense. Kami harap Anda merasa informatif dan bermanfaat. Kami mendorong Anda untuk bereksperimen dengan teknik-teknik ini dalam proyek Anda sendiri dan membagikan temuan Anda dengan komunitas.