Buka kekuatan custom hook React dan komposisi efek untuk mengelola efek samping yang kompleks dalam aplikasi Anda. Pelajari cara mengorkestrasi efek untuk kode yang lebih bersih dan mudah dipelihara.
Komposisi Efek Custom Hook React: Menguasai Orkestrasi Efek yang Kompleks
Custom hook React telah merevolusi cara kita mengelola logika stateful dan efek samping dalam aplikasi kita. Meskipun useEffect
adalah alat yang kuat, komponen yang kompleks dapat dengan cepat menjadi sulit diatur dengan beberapa efek yang saling terkait. Di sinilah komposisi efek berperan – sebuah teknik yang memungkinkan kita untuk memecah efek kompleks menjadi custom hook yang lebih kecil dan dapat digunakan kembali, menghasilkan kode yang lebih bersih dan lebih mudah dipelihara.
Apa itu Komposisi Efek?
Komposisi efek adalah praktik menggabungkan beberapa efek yang lebih kecil, biasanya dienkapsulasi dalam custom hook, untuk menciptakan efek yang lebih besar dan lebih kompleks. Daripada menjejalkan semua logika ke dalam satu panggilan useEffect
, kita membuat unit fungsionalitas yang dapat digunakan kembali yang dapat disusun bersama sesuai kebutuhan. Pendekatan ini mendorong penggunaan kembali kode, meningkatkan keterbacaan, dan menyederhanakan pengujian.
Mengapa Menggunakan Komposisi Efek?
Ada beberapa alasan kuat untuk mengadopsi komposisi efek dalam proyek React Anda:
- Peningkatan Penggunaan Kembali Kode: Custom hook dapat digunakan kembali di beberapa komponen, mengurangi duplikasi kode dan meningkatkan kemudahan pemeliharaan.
- Peningkatan Keterbacaan: Memecah efek kompleks menjadi unit-unit yang lebih kecil dan terfokus membuat kode lebih mudah dipahami dan dinalar.
- Pengujian yang Disederhanakan: Efek yang lebih kecil dan terisolasi lebih mudah untuk diuji dan di-debug.
- Peningkatan Modularitas: Komposisi efek mendorong arsitektur modular, membuatnya lebih mudah untuk menambah, menghapus, atau memodifikasi fungsionalitas tanpa memengaruhi bagian lain dari aplikasi.
- Pengurangan Kompleksitas: Mengelola sejumlah besar efek samping dalam satu
useEffect
dapat menyebabkan kode spaghetti. Komposisi efek membantu memecah kompleksitas menjadi bagian-bagian yang dapat dikelola.
Contoh Dasar: Menggabungkan Pengambilan Data dan Persistensi Local Storage
Mari kita pertimbangkan skenario di mana kita perlu mengambil data pengguna dari API dan menyimpannya ke local storage. Tanpa komposisi efek, kita mungkin akan berakhir dengan satu useEffect
yang menangani kedua tugas tersebut. Berikut cara kita dapat mencapai hasil yang sama dengan komposisi efek:
1. Membuat Hook useFetchData
Hook ini bertanggung jawab untuk mengambil data dari API.
import { useState, useEffect } from 'react';
function useFetchData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const json = await response.json();
setData(json);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetchData;
2. Membuat Hook useLocalStorage
Hook ini menangani persistensi data ke local storage.
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
export default useLocalStorage;
3. Mengomposisikan Hook dalam Komponen
Sekarang kita dapat mengomposisikan hook ini dalam komponen untuk mengambil data pengguna dan menyimpannya ke local storage.
import React from 'react';
import useFetchData from './useFetchData';
import useLocalStorage from './useLocalStorage';
function UserProfile() {
const { data: userData, loading, error } = useFetchData('https://api.example.com/user/profile');
const [storedUserData, setStoredUserData] = useLocalStorage('userProfile', null);
useEffect(() => {
if (userData) {
setStoredUserData(userData);
}
}, [userData, setStoredUserData]);
if (loading) {
return Memuat profil pengguna...
;
}
if (error) {
return Kesalahan saat mengambil profil pengguna: {error.message}
;
}
if (!userData && !storedUserData) {
return Tidak ada data pengguna yang tersedia.
;
}
const userToDisplay = storedUserData || userData;
return (
Profil Pengguna
Nama: {userToDisplay.name}
Email: {userToDisplay.email}
);
}
export default UserProfile;
Dalam contoh ini, kita telah memisahkan logika pengambilan data dan logika persistensi local storage menjadi dua custom hook yang terpisah. Komponen UserProfile
kemudian mengomposisikan hook ini untuk mencapai fungsionalitas yang diinginkan. Pendekatan ini membuat kode lebih modular, dapat digunakan kembali, dan lebih mudah diuji.
Contoh Lanjutan: Mengorkestrasi Efek Kompleks
Komposisi efek menjadi lebih kuat saat menangani skenario yang lebih kompleks. Mari kita jelajahi beberapa contoh lanjutan.
1. Mengelola Langganan dan Event Listener
Pertimbangkan skenario di mana Anda perlu berlangganan WebSocket dan mendengarkan event tertentu. Anda juga perlu menangani pembersihan saat komponen di-unmount. Berikut cara Anda dapat menggunakan komposisi efek untuk mengelola ini:
a. Membuat Hook useWebSocket
Hook ini membangun koneksi WebSocket dan menangani logika koneksi ulang.
import { useState, useEffect, useRef } from 'react';
function useWebSocket(url) {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);
const retryCount = useRef(0);
useEffect(() => {
const connect = () => {
const newSocket = new WebSocket(url);
newSocket.onopen = () => {
console.log('WebSocket terhubung');
setIsConnected(true);
retryCount.current = 0;
};
newSocket.onclose = () => {
console.log('WebSocket terputus');
setIsConnected(false);
// Exponential backoff untuk koneksi ulang
const timeout = Math.min(3000 * Math.pow(2, retryCount.current), 60000);
retryCount.current++;
console.log(`Menyambungkan kembali dalam ${timeout/1000} detik...`);
setTimeout(connect, timeout);
};
newSocket.onerror = (error) => {
console.error('Kesalahan WebSocket:', error);
};
setSocket(newSocket);
};
connect();
return () => {
if (socket) {
socket.close();
}
};
}, [url]);
return { socket, isConnected };
}
export default useWebSocket;
b. Membuat Hook useEventListener
Hook ini memungkinkan Anda untuk dengan mudah mendengarkan event tertentu di WebSocket.
import { useEffect } from 'react';
function useEventListener(socket, eventName, handler) {
useEffect(() => {
if (!socket) return;
const listener = (event) => handler(event);
socket.addEventListener(eventName, listener);
return () => {
socket.removeEventListener(eventName, listener);
};
}, [socket, eventName, handler]);
}
export default useEventListener;
c. Mengomposisikan Hook dalam Komponen
import React, { useState } from 'react';
import useWebSocket from './useWebSocket';
import useEventListener from './useEventListener';
function WebSocketComponent() {
const { socket, isConnected } = useWebSocket('wss://echo.websocket.events');
const [message, setMessage] = useState('');
const [receivedMessages, setReceivedMessages] = useState([]);
useEventListener(socket, 'message', (event) => {
setReceivedMessages((prevMessages) => [...prevMessages, event.data]);
});
const sendMessage = () => {
if (socket && isConnected) {
socket.send(message);
setMessage('');
}
};
return (
Contoh WebSocket
Status Koneksi: {isConnected ? 'Terhubung' : 'Terputus'}
setMessage(e.target.value)}
placeholder="Masukkan pesan"
/>
Pesan Diterima:
{receivedMessages.map((msg, index) => (
- {msg}
))}
);
}
export default WebSocketComponent;
Dalam contoh ini, useWebSocket
mengelola koneksi WebSocket, termasuk logika koneksi ulang, sementara useEventListener
menyediakan cara yang bersih untuk berlangganan event tertentu. WebSocketComponent
mengomposisikan hook ini untuk membuat klien WebSocket yang berfungsi penuh.
2. Mengorkestrasi Operasi Asinkron dengan Dependensi
Terkadang, efek perlu dipicu dalam urutan tertentu atau berdasarkan dependensi tertentu. Katakanlah Anda perlu mengambil data pengguna, lalu mengambil postingan mereka berdasarkan ID pengguna, dan kemudian memperbarui UI. Anda dapat menggunakan komposisi efek untuk mengorkestrasi operasi asinkron ini.
a. Membuat Hook useUserData
Hook ini mengambil data pengguna.
import { useState, useEffect } from 'react';
function useUserData(userId) {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const json = await response.json();
setUserData(json);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [userId]);
return { userData, loading, error };
}
export default useUserData;
b. Membuat Hook useUserPosts
Hook ini mengambil postingan pengguna berdasarkan ID pengguna.
import { useState, useEffect } from 'react';
function useUserPosts(userId) {
const [userPosts, setUserPosts] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
if (!userId) {
setUserPosts(null);
setLoading(false);
return;
}
const fetchPosts = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}/posts`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const json = await response.json();
setUserPosts(json);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
fetchPosts();
}, [userId]);
return { userPosts, loading, error };
}
export default useUserPosts;
c. Mengomposisikan Hook dalam Komponen
import React, { useState } from 'react';
import useUserData from './useUserData';
import useUserPosts from './useUserPosts';
function UserProfileWithPosts() {
const [userId, setUserId] = useState(1); // Mulai dengan ID pengguna default
const { userData, loading: userLoading, error: userError } = useUserData(userId);
const { userPosts, loading: postsLoading, error: postsError } = useUserPosts(userId);
return (
Profil Pengguna dengan Postingan
setUserId(parseInt(e.target.value, 10))}
/>
{userLoading ? Memuat data pengguna...
: null}
{userError ? Kesalahan memuat data pengguna: {userError.message}
: null}
{userData ? (
Detail Pengguna
Nama: {userData.name}
Email: {userData.email}
) : null}
{postsLoading ? Memuat postingan pengguna...
: null}
{postsError ? Kesalahan memuat postingan pengguna: {postsError.message}
: null}
{userPosts ? (
Postingan Pengguna
{userPosts.map((post) => (
- {post.title}
))}
) : null}
);
}
export default UserProfileWithPosts;
Dalam contoh ini, useUserPosts
bergantung pada userId
. Hook hanya mengambil postingan ketika userId
yang valid tersedia. Ini memastikan bahwa efek dipicu dalam urutan yang benar dan UI diperbarui sebagaimana mestinya.
Praktik Terbaik untuk Komposisi Efek
Untuk memanfaatkan komposisi efek secara maksimal, pertimbangkan praktik terbaik berikut:
- Prinsip Tanggung Jawab Tunggal: Setiap custom hook harus memiliki satu tanggung jawab yang terdefinisi dengan baik.
- Nama Deskriptif: Gunakan nama deskriptif untuk custom hook Anda untuk menunjukkan tujuannya dengan jelas.
- Array Dependensi: Kelola array dependensi dalam panggilan
useEffect
Anda dengan hati-hati untuk menghindari render ulang yang tidak perlu atau loop tak terbatas. - Pengujian: Tulis unit test untuk custom hook Anda untuk memastikan mereka berperilaku seperti yang diharapkan.
- Dokumentasi: Dokumentasikan custom hook Anda agar lebih mudah dipahami dan digunakan kembali.
- Hindari Abstraksi Berlebihan: Jangan merekayasa custom hook Anda secara berlebihan. Jaga agar tetap sederhana dan fokus.
- Pertimbangkan Penanganan Kesalahan: Terapkan penanganan kesalahan yang kuat di custom hook Anda untuk menangani situasi tak terduga dengan baik.
Pertimbangan Global
Saat mengembangkan aplikasi React untuk audiens global, perhatikan pertimbangan berikut:
- Internasionalisasi (i18n): Gunakan library seperti
react-intl
ataui18next
untuk mendukung berbagai bahasa. - Lokalisasi (l10n): Sesuaikan aplikasi Anda dengan preferensi regional yang berbeda, seperti format tanggal dan angka.
- Aksesibilitas (a11y): Pastikan aplikasi Anda dapat diakses oleh pengguna penyandang disabilitas dengan mengikuti pedoman WCAG.
- Performa: Optimalkan aplikasi Anda untuk kondisi jaringan dan kemampuan perangkat yang berbeda. Pertimbangkan untuk menggunakan teknik seperti pemisahan kode (code splitting) dan pemuatan lambat (lazy loading).
- Content Delivery Network (CDN): Gunakan CDN untuk mengirimkan aset aplikasi Anda dari server yang berlokasi lebih dekat dengan pengguna Anda, mengurangi latensi dan meningkatkan performa.
- Zona Waktu: Saat berurusan dengan tanggal dan waktu, perhatikan zona waktu yang berbeda dan gunakan library yang sesuai seperti
moment-timezone
ataudate-fns-timezone
.
Contoh: Pemformatan Tanggal yang Diinternasionalkan
import { useIntl, FormattedDate } from 'react-intl';
function MyComponent() {
const intl = useIntl();
const now = new Date();
return (
Tanggal Saat Ini:
Tanggal Saat Ini (Jerman):
);
}
export default MyComponent;
Kesimpulan
Komposisi efek adalah teknik yang kuat untuk mengelola efek samping yang kompleks dalam aplikasi React. Dengan memecah efek besar menjadi custom hook yang lebih kecil dan dapat digunakan kembali, Anda dapat meningkatkan penggunaan kembali kode, meningkatkan keterbacaan, menyederhanakan pengujian, dan mengurangi kompleksitas secara keseluruhan. Terapkan komposisi efek untuk membuat aplikasi React yang lebih bersih, lebih mudah dipelihara, dan dapat diskalakan untuk audiens global.