Yuk Pahami Selector Zustand Biar Performa Aplikasi Optimal
Dalam pengembangan aplikasi modern, pengelolaan state adalah salah satu kunci performa. Library seperti Zustand hadir untuk menyederhanakan tugas ini, namun potensi optimalisasi performa seringkali tersembunyi dalam detail implementasinya. Salah satu detail krusial yang perlu kita pahami betul adalah penggunaan selector. Untuk itu, yuk pahami selector Zustand biar performa aplikasi optimal dan responsif.
Kita semua tahu betapa frustrasinya melihat komponen React melakukan re render yang tidak perlu. Itu bukan hanya membuang siklus CPU, tetapi juga bisa membuat antarmuka pengguna terasa lambat. Di sinilah selector Zustand berperan sebagai pahlawan tanpa tanda jasa, membantu kita mengisolasi hanya bagian state yang benar benar relevan untuk sebuah komponen, dan memastikan re render hanya terjadi saat benar benar dibutuhkan.
Kenapa Selector Penting untuk Performa Aplikasi Anda?
Bayangkan sebuah toko state global yang berisi banyak informasi: data pengguna, pengaturan tema, status loading, dan daftar item belanja. Jika kita mengambil seluruh objek state setiap kali komponen membutuhkan sepotong kecil informasi, komponen tersebut akan re render setiap kali bagian mana pun dari state berubah. Ini seperti menerima seluruh koran hanya untuk membaca satu baris berita utama.
Zustand dirancang dengan filosofi minimalis dan fokus pada performa. Secara internal, Zustand menggunakan perbandingan referensi (atau Object.is) untuk menentukan apakah sebuah state baru berbeda dari state sebelumnya. Jika kita hanya memilih satu nilai primitif seperti store.count, perbandingannya sederhana. Namun, saat kita mulai memilih objek atau array, bahkan jika isinya sama, referensinya bisa berbeda, memicu re render yang tidak perlu.
Inilah mengapa selector menjadi fundamental. Dengan selector, kita secara eksplisit memberitahu Zustand bagian mana dari state yang kita pedalkan peduli. Saat state berubah, Zustand akan menjalankan selector ini. Jika output selector tidak berubah menurut fungsi perbandingan yang digunakan, komponen yang mengonsumsi selector tersebut tidak akan re render. Ini adalah mekanisme yang kuat untuk mengoptimalkan re render dan menjaga aplikasi tetap cepat dan lancar.
Memahami Dasar Selector di Zustand
Selector dalam Zustand pada dasarnya adalah fungsi yang kita berikan ke hook useStore. Fungsi ini menerima objek state penuh sebagai argumen dan mengembalikan bagian dari state yang kita inginkan.
Mari kita lihat contoh sederhana. Misalkan kita punya store seperti ini:
javascript
import { create } from 'zustand';Jika sebuah komponen hanya ingin menampilkan jumlah beruang, kita bisa menggunakan selector seperti ini:
javascript
function BearCounter() {
const bearCount = useBearStore(state => state.bears);
// ...
}Dalam kasus ini, state => state.bears adalah selector kita. Komponen BearCounter hanya akan re render jika nilai state.bears berubah. Jika state.food berubah, komponen ini tidak akan terpengaruh, karena selectornya tidak bergantung pada state.food. Asik, bukan?
Meningkatkan Presisi dengan Shallow Equality
Masalah muncul saat selector kita mengembalikan objek atau array yang berbeda referensi setiap kali, meskipun isinya sama. Misalnya, kita ingin mengambil objek food dari store:
javascript
function FoodDisplay() {
const foodItems = useBearStore(state => state.food);
// ...
}Jika kita memanggil addBerries() yang mengubah objek food secara imutabel, foodItems akan menjadi objek baru. Karena Object.is (perbandingan default Zustand) hanya membandingkan referensi, FoodDisplay akan re render meskipun kita mungkin hanya peduli pada berries atau fish di dalamnya, dan objek food secara keseluruhan mungkin tidak benar benar "berbeda" secara semantik jika hanya satu properti yang berubah.
Untuk mengatasi ini, Zustand menyediakan shallow equality function dari zustand/shallow. Ini melakukan perbandingan dangkal pada properti tingkat pertama dari objek yang dikembalikan oleh selector.
javascript
import { create } from 'zustand';
import { shallow } from 'zustand/shallow'; // Import shallowconst useBearStore = create(set => ({
bears: 0,
food: {
berries: 10,
fish: 5,
},
increaseBears: () => set(state => ({ bears: state.bears + 1 })),
addBerries: () => set(state => ({
food: {
...state.food,
berries: state.food.berries + 1
}
})),
removeFish: () => set(state => ({
food: {
...state.food,
fish: state.food.fish > 0 ? state.food.fish - 1 : 0
}
})),
}));function FoodDisplay() {
// Menggunakan shallow untuk membandingkan objek food
const { berries, fish } = useBearStore(
state => ({ berries: state.food.berries, fish: state.food.fish }),
shallow // Lewatkan shallow sebagai fungsi perbandingan
);return (
Berries: {berries}
Fish: {fish}
Add Berries
Remove Fish
);
}Pada contoh di atas, kita mengembalikan objek baru { berries: state.food.berries, fish: state.food.fish } dari selector. Tanpa shallow, komponen ini akan re render setiap kali objek food secara keseluruhan berubah referensinya. Dengan shallow, komponen hanya akan re render jika nilai berries atau fish itu sendiri berubah. Ini sangat efektif untuk sebagian besar kasus di mana kita perlu memilih beberapa properti dari objek state.
Menggunakan Custom Equality Functions
Ada kalanya shallow tidak cukup. Misalnya, jika kita memiliki struktur state yang sangat bersarang atau perlu membandingkan objek yang lebih kompleks secara mendalam. Dalam kasus ini, kita bisa menyediakan fungsi perbandingan kustom kita sendiri.
Fungsi perbandingan ini akan menerima (a, b) di mana a adalah nilai yang dikembalikan oleh selector pada re render sebelumnya dan b adalah nilai yang dikembalikan oleh selector saat ini. Fungsi harus mengembalikan true jika kedua nilai dianggap sama dan false jika berbeda.
javascript
import { create } from 'zustand';
import { shallow } from 'zustand/shallow';// Misalnya, kita punya struktur state yang lebih kompleks
const useComplexStore = create(set => ({
user: {
id: '123',
profile: {
name: 'John Doe',
email: '[email protected]',
settings: {
theme: 'dark',
notifications: true,
},
},
},
updateUserName: (newName) => set(state => ({
user: {
...state.user,
profile: {
...state.user.profile,
name: newName,
},
},
})),
toggleNotifications: () => set(state => ({
user: {
...state.user,
profile: {
...state.user.profile,
settings: {
...state.user.profile.settings,
notifications: !state.user.profile.settings.notifications,
},
},
},
})),
}));// Fungsi perbandingan dalam (deep equality) sederhana,
// dalam aplikasi nyata bisa pakai library seperti lodash.isequal
const deepEqual = (objA, objB) => {
if (objA === objB) return true;
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) return false;const keysA = Object.keys(objA);
const keysB = Object.keys(objB);if (keysA.length !== keysB.length) return false;for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
if (!keysB.includes(key) || !deepEqual(objA[key], objB[key])) {
return false;
}
}
return true;
};function UserSettings() {
// Memilih seluruh objek settings dan menggunakan deep equality
const userSettings = useComplexStore(
state => state.user.profile.settings,
deepEqual // Gunakan fungsi perbandingan kustom kita
);return (
Theme: {userSettings.theme}
Notifications: {userSettings.notifications ? 'On' : 'Off'}
Toggle Notifications
);
}Menggunakan fungsi perbandingan kustom memberi kita fleksibilitas penuh. Namun, perlu diingat bahwa perbandingan yang terlalu dalam (deep equality) bisa mahal dari segi performa, terutama untuk objek besar. Kita harus menimbang antara kompleksitas perbandingan dan frekuensi perubahan state. Seringkali, restrukturisasi state atau memecah selector menjadi lebih granular adalah pendekatan yang lebih baik daripada menggunakan deep equality yang mahal.
Strategi Lanjutan dan Best Practice untuk Selector Optimal
Untuk benar benar Yuk Pahami Selector Zustand Biar Performa Aplikasi Optimal, kita perlu mengadopsi beberapa strategi dan best practice:
Menggunakan Multiple Selector per Komponen
Seringkali, lebih baik memecah kebutuhan state komponen menjadi beberapa pemanggilan useStore daripada satu selector besar yang mengembalikan objek. Setiap pemanggilan useStore dengan selectornya sendiri akan memiliki langganan terpisah ke store. Ini memastikan bahwa komponen hanya re render jika salah satu dari nilai yang dipedulikan berubah.
javascript
function UserProfile() {
const userName = useComplexStore(state => state.user.profile.name);
const userEmail = useComplexStore(state => state.user.profile.email);
// ...
}Dalam contoh ini, jika hanya userName yang berubah, komponen UserProfile akan re render karena userName adalah nilai primitif yang dibandingkan dengan Object.is. Jika kita menggabungkan keduanya menjadi satu objek dengan shallow, seperti { name: state.user.profile.name, email: state.user.profile.email }, komponen akan re render jika salah satu dari name atau email berubah. Keduanya adalah pendekatan yang valid, tergantung seberapa erat kedua nilai tersebut terkait.
Memoizing Selector Kompleks
Jika selector kita melakukan komputasi yang mahal atau mengembalikan objek/array baru setiap kali dipanggil (misalnya, memfilter daftar atau mengolah data), kita bisa memoize selector itu sendiri. Meskipun Zustand cukup cerdas, memastikan selector Anda stabil akan lebih baik.
Kita bisa menggunakan useCallback untuk memastikan fungsi selector itu sendiri stabil antar re render komponen:
javascript
import { create } from 'zustand';
import { useCallback } from 'react';const useItemStore = create(set => ({
items: [
{ id: 1, name: 'Apple', category: 'Fruit' },
{ id: 2, name: 'Banana', category: 'Fruit' },
{ id: 3, name: 'Carrot', category: 'Vegetable' },
],
filter: 'All',
setFilter: (newFilter) => set({ filter: newFilter }),
}));function FilteredItemList() {
const filter = useItemStore(state => state.filter);// Memoize selector function
const filteredItemsSelector = useCallback(
state => state.items.filter(item => filter === 'All' || item.category === filter),
[filter] // Dependency array untuk useCallback
);const filteredItems = useItemStore(filteredItemsSelector, shallow); // Gunakan shallow untuk perbandingan hasilnyareturn (
useItemStore.getState().setFilter('Fruit')}>Show Fruits
useItemStore.getState().setFilter('Vegetable')}>Show Vegetables
useItemStore.getState().setFilter('All')}>Show All
{filteredItems.map(item => {item.name})}
);
}Pendekatan lain yang lebih canggih adalah menggunakan pola createSelector yang populer dari library seperti Reselect. Zustand sendiri tidak menyediakannya secara bawaan, tetapi kita bisa mengintegrasikannya dengan mudah. createSelector akan memoize hasil dari selector gabungan, memastikan bahwa fungsi komputasi hanya berjalan jika inputnya benar benar berubah.
Menghindari Objek Literal Baru dalam Selector Tanpa Equality Function
Kesalahan umum yang sering terjadi adalah mengembalikan objek literal baru ({}) atau array literal baru ([]) dari selector tanpa menyediakan fungsi perbandingan shallow atau kustom.
javascript
// HINDARI ini tanpa shallow
const { valueA, valueB } = useMyStore(state => ({
valueA: state.some.path.A,
valueB: state.some.path.B
}));
// Komponen akan re render setiap kali state berubah, bahkan jika valueA dan valueB tidak berubah,
// karena objek literal baru selalu memiliki referensi yang berbeda.Selalu ingat: jika selector Anda mengembalikan objek atau array yang dibuat "saat itu juga", Anda hampir selalu membutuhkan fungsi perbandingan (shallow atau kustom) untuk mencegah re render yang tidak diinginkan.
Kapan Tidak Menggunakan Selector yang Kompleks
Tidak semua kasus membutuhkan selector yang rumit. Jika Anda hanya mengambil nilai primitif tunggal, selector sederhana state => state.someValue sudah optimal. Jangan over engineer. Tujuannya adalah performa, bukan kompleksitas yang tidak perlu.
Menggabungkan State Tanpa Re render Berlebihan
Terkadang kita butuh beberapa potongan state yang berbeda dalam satu komponen. Menggabungkan semuanya dalam satu selector dengan shallow adalah cara yang baik, seperti yang kita lihat pada contoh FoodDisplay. Ini akan membuat komponen hanya re render jika salah satu dari potongan state yang digabungkan itu berubah, dibandingkan dengan banyak pemanggilan useStore terpisah yang mungkin memicu re render parsial beberapa kali.
Kesalahan Umum yang Harus Dihindari
Untuk benar benar menguasai optimalisasi performa dengan selector Zustand, ada beberapa perangkap yang harus kita hindari:
- Mengembalikan Objek Baru Tanpa Perbandingan: Ini adalah kesalahan paling umum. Jika selector Anda mengembalikan
{},[], ataunew SomeClass()tanpashallowatau fungsi perbandingan kustom, komponen Anda akan re render pada setiap perubahan state, mengalahkan tujuan selector.
Terlalu Banyak Memilih State: Memilih seluruh objek state hanya karena Anda butuh satu propertinya akan memicu re render yang tidak perlu. Selalu pilih hanya* yang Anda butuhkan.
- Logika Kompleks Langsung di Selector Tanpa Memoization: Jika selector melakukan komputasi yang berat (misalnya, iterasi besar, transformasi data kompleks) dan tidak dimemoize (baik dengan
useCallbackuntuk selector function itu sendiri atau dengancreateSelectordari library eksternal), komputasi tersebut akan berjalan pada setiap re render komponen, bahkan jika hasilnya tidak berubah. - Mutasi State di Selector: Ingat, selector hanya boleh membaca state dan mengembalikan nilai. Jangan pernah memutasi state secara langsung di dalam selector. Ini akan menyebabkan bug dan perilaku yang tidak terduga.
Kesimpulan
Memahami dan menerapkan selector Zustand dengan benar adalah langkah krusial untuk memastikan Yuk Pahami Selector Zustand Biar Performa Aplikasi Optimal. Dengan memilih hanya bagian state yang relevan, kita bisa secara drastis mengurangi re render komponen yang tidak perlu, sehingga aplikasi kita terasa lebih cepat dan responsif.
Kita sudah melihat bagaimana selector dasar bekerja, pentingnya shallow untuk perbandingan objek/array, serta kapan harus menggunakan fungsi perbandingan kustom. Selain itu, kita membahas strategi lanjutan seperti memecah selector, memoize komputasi kompleks, dan menghindari kesalahan umum. Kuncinya adalah pemikiran yang cermat tentang apa yang sebenarnya dibutuhkan komponen kita dan bagaimana kita membandingkan nilai state yang berubah. Dengan praktik terbaik ini, kita bisa membangun aplikasi yang tidak hanya fungsional tetapi juga sangat efisien.