Mengadopsi Signals di Aplikasi React yang Ada: Reaktivitas Granular Tanpa Migrasi Penuh
1. Pendahuluan
Di dunia pengembangan web yang bergerak cepat, menjaga aplikasi tetap responsif dan berkinerja tinggi adalah kunci utama. React telah menjadi pilihan populer selama bertahun-tahun, namun seiring bertambahnya kompleksitas aplikasi, isu performa yang terkait dengan re-rendering yang tidak perlu seringkali menjadi tantangan. Kita semua akrab dengan useState, useEffect, useMemo, dan useCallback sebagai alat untuk mengelola state dan mengoptimalkan re-render. Tapi bagaimana jika ada cara lain yang lebih granular dan efisien untuk mengelola reaktivitas, bahkan di dalam aplikasi React yang sudah ada?
Masuklah Signals. Konsep ini, yang dipopulerkan oleh framework seperti SolidJS dan Preact, menawarkan pendekatan reaktivitas yang berbeda: bukan lagi seluruh komponen yang di-render ulang ketika state berubah, melainkan hanya bagian-bagian spesifik yang bergantung pada state tersebut. 🚀 Bayangkan aplikasi Anda terasa lebih cepat dan smooth tanpa perlu migrasi besar-besaran ke framework baru. Artikel ini akan memandu Anda cara mengadopsi Signals secara bertahap di aplikasi React Anda yang sudah ada, memungkinkan Anda mencicipi manfaat performa tanpa drama.
2. Memahami Kembali Reaktivitas di React dan Tantangannya
Sebelum menyelam ke Signals, mari kita segarkan kembali bagaimana React menangani reaktivitas.
Di React, ketika state atau props suatu komponen berubah, React akan menjadwalkan ulang render komponen tersebut. Ini adalah model “push-based”: perubahan state “mendorong” komponen untuk di-render ulang. Jika komponen induk di-render ulang, secara default semua komponen anaknya juga akan di-render ulang, kecuali jika kita mengoptimalkannya secara manual dengan React.memo, useMemo, atau useCallback.
⚠️ Tantangan umum:
- Re-render Berlebihan: Seringkali, perubahan state di bagian atas pohon komponen dapat memicu re-render di seluruh subtree, meskipun banyak komponen anak tidak benar-benar menggunakan state yang berubah. Ini membuang-buang siklus CPU dan dapat menyebabkan UI terasa lambat.
- “Prop Drilling”: Melewatkan props melalui banyak lapisan komponen hanya untuk mencapai komponen yang membutuhkannya, yang memperparah masalah re-render dan membuat kode sulit dipelihara.
- Kompleksitas Memoization: Untuk mengatasi re-render berlebihan, developer harus secara manual “memoize” komponen, fungsi, dan nilai. Ini menambah boilerplate, terkadang sulit untuk dilakukan dengan benar, dan bahkan bisa menimbulkan overhead tersendiri jika tidak digunakan dengan bijak.
React terus berinovasi dengan fitur seperti Concurrent React dan React Server Components untuk mengatasi tantangan ini. Namun, Signals menawarkan solusi komplementer yang bekerja di level yang lebih rendah, yaitu pada bagaimana data bereaksi terhadap perubahan.
3. Apa itu Signals dan Mengapa Mereka Menarik?
Signals adalah primitif reaktif yang memungkinkan Anda mendeklarasikan nilai yang dapat berubah seiring waktu. Yang membedakannya adalah sifat “pull-based” dan granularitasnya.
💡 Konsep utama Signals:
- Granularitas: Ketika nilai sebuah signal berubah, hanya kode yang secara langsung bergantung pada signal tersebut yang akan dieksekusi ulang. Bukan seluruh komponen, bukan seluruh subtree.
- Pull-Based: Alih-alih “mendorong” perubahan ke komponen (seperti React), kode yang menggunakan signal “menarik” nilai terbaru hanya ketika dibutuhkan.
- Dependency Tracking Otomatis: Sistem Signals secara otomatis melacak dependensi. Anda tidak perlu secara manual mendeklarasikan dependensi seperti di
useEffectatauuseMemo. Ini mengurangi boilerplate dan potensi bug.
Analogi: Bayangkan React seperti sistem siaran televisi. Ketika ada berita penting (perubahan state), seluruh stasiun (aplikasi) akan menyiarkan ulang program (re-render komponen), dan semua penonton (komponen anak) harus menonton ulang, bahkan jika mereka hanya tertarik pada satu segmen berita.
Signals, di sisi lain, seperti sistem notifikasi yang sangat cerdas. Anda berlangganan hanya untuk topik berita spesifik yang Anda minati. Ketika ada pembaruan pada topik tersebut, Anda hanya menerima notifikasi untuk topik itu, dan hanya Anda yang memperbarui informasi yang relevan. Tidak ada siaran ulang program yang tidak perlu.
✅ Manfaat Signals:
- Performa Unggul: Mengurangi re-render yang tidak perlu secara drastis, terutama di aplikasi dengan banyak interaktivitas atau data yang sering berubah.
- Kesederhanaan: Mengurangi kebutuhan untuk
useMemo,useCallback, danReact.memosecara manual, membuat kode lebih bersih dan mudah dipahami. - Otomatisasi: Pelacakan dependensi otomatis berarti lebih sedikit bug terkait dependensi yang hilang.
4. Memilih Implementasi Signals untuk React
Ada beberapa library yang menyediakan implementasi Signals yang dapat Anda gunakan di React. Salah satu yang paling populer dan dioptimalkan untuk React adalah @preact/signals-react. Library ini memungkinkan Anda menggunakan API Signals di dalam komponen React Anda dengan mulus, sambil tetap memanfaatkan mekanisme render React.
📌 Pilihan umum:
@preact/signals-react: Paling direkomendasikan untuk React. Menyediakan integrasi yang mulus sehingga komponen React Anda dapat “bereaksi” terhadap Signals tanpa re-render berlebihan.@web-std/signals: Implementasi standar yang lebih agnostik terhadap framework. Anda bisa membangun lapisan integrasi sendiri, namun@preact/signals-reactlebih praktis untuk sebagian besar kasus.- Custom Implementation: Jika Anda ingin kontrol penuh dan memahami mekanismenya secara mendalam, Anda bisa membuat implementasi Signals sendiri. Namun, ini tidak disarankan untuk adopsi cepat di aplikasi yang sudah ada.
Untuk artikel ini, kita akan fokus pada @preact/signals-react karena kemudahan penggunaannya dan integrasi yang kuat dengan React.
5. Langkah-Langkah Mengadopsi Signals di Aplikasi React yang Ada
Mari kita mulai dengan langkah-langkah praktis.
5.1. Instalasi
Pertama, tambahkan library @preact/signals-react ke proyek Anda:
npm install @preact/signals-react
# atau
yarn add @preact/signals-react
5.2. Membuat Signal Sederhana
Anda bisa membuat signal di mana saja, bahkan di luar komponen React Anda. Ini adalah salah satu kekuatan Signals: mereka adalah primitif reaktif yang agnostik terhadap UI.
// src/store/counter.ts
import { signal } from '@preact/signals-react';
export const count = signal(0); // Inisialisasi signal dengan nilai awal 0
export const isEven = signal(false); // Signal lain
// Anda bisa membuat computed signal di sini juga
export const doubledCount = signal(count.value * 2); // Ini akan diupdate secara otomatis
// Untuk mengupdate, gunakan .value
export function increment() {
count.value++;
}
export function decrement() {
count.value--;
}
5.3. Menggunakan Signal di Komponen React
Untuk menggunakan signal di komponen React, Anda cukup mengakses properti .value dari signal tersebut. @preact/signals-react akan secara otomatis mendeteksi penggunaan signal ini dan memastikan komponen Anda di-render ulang hanya jika nilai signal yang diakses berubah.
// src/components/CounterDisplay.tsx
import React from 'react';
import { count, increment, decrement } from '../store/counter';
function CounterDisplay() {
// Cukup akses nilai signal dengan .value
// Komponen ini akan di-render ulang HANYA ketika `count.value` berubah
console.log('CounterDisplay rendered');
return (
<div>
<p>Current Count: {count.value}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
}
export default CounterDisplay;
Anda juga bisa membuat signal lokal di dalam komponen jika Anda mau, meskipun kekuatan utama Signals seringkali terlihat pada state global atau shared.
// src/components/LocalCounter.tsx
import React from 'react';
import { signal } from '@preact/signals-react';
function LocalCounter() {
const localCount = signal(0);
const incrementLocal = () => {
localCount.value++;
};
return (
<div>
<p>Local Count: {localCount.value}</p>
<button onClick={incrementLocal}>Increment Local</button>
</div>
);
}
export default LocalCounter;
5.4. computed Signals untuk Derived State
computed signals memungkinkan Anda membuat nilai yang secara otomatis dihitung ulang ketika signal yang menjadi dependensinya berubah. Ini adalah pengganti yang sangat efisien untuk useMemo.
// src/store/counter.ts (lanjutan)
import { signal, computed } from '@preact/signals-react';
export const count = signal(0);
// `computed` akan secara otomatis melacak `count` sebagai dependensi
export const doubledCount = computed(() => count.value * 2);
export const isEven = computed(() => count.value % 2 === 0);
export function increment() {
count.value++;
}
export function decrement() {
count.value--;
}
Sekarang, Anda bisa menggunakan doubledCount.value atau isEven.value di komponen Anda, dan komponen akan di-render ulang hanya jika nilai computed tersebut berubah.
// src/components/CounterStatus.tsx
import React from 'react';
import { count, doubledCount, isEven } from '../store/counter';
function CounterStatus() {
console.log('CounterStatus rendered');
return (
<div>
<p>Count: {count.value}</p>
<p>Doubled Count: {doubledCount.value}</p> {/* Diupdate hanya jika count berubah */}
<p>Is Even: {isEven.value ? 'Yes' : 'No'}</p> {/* Diupdate hanya jika isEven berubah */}
</div>
);
}
export default CounterStatus;
5.5. effect Signals untuk Side Effects
effect signals mirip dengan useEffect tetapi dengan pelacakan dependensi otomatis. Ini akan menjalankan fungsi setiap kali signal yang diakses di dalamnya berubah.
// src/store/logger.ts
import { effect } from '@preact/signals-react';
import { count, isEven } from './counter';
// Effect ini akan berjalan setiap kali `count.value` atau `isEven.value` berubah
effect(() => {
console.log(`Log: Count is now ${count.value}, and it is ${isEven.value ? 'even' : 'odd'}`);
});
effect ini sangat berguna untuk side effects seperti logging, sinkronisasi ke localStorage, atau memicu API call (meskipun untuk API call, seringkali lebih baik mengelolanya dalam mekanisme data fetching seperti React Query yang dapat berinteraksi dengan Signals).
6. Studi Kasus: Mengoptimalkan Komponen Pencarian
Mari kita lihat contoh nyata di mana Signals dapat meningkatkan performa. Bayangkan Anda memiliki komponen daftar produk dengan fitur pencarian dan filter.
Contoh Awal (Hanya React useState/useEffect)
// src/components/ProductListTraditional.tsx
import React, { useState, useEffect, useMemo } from 'react';
const allProducts = [
{ id: 1, name: 'Laptop', category: 'Electronics', price: 1200 },
{ id: 2, name: 'Mouse', category: 'Electronics', price: 25 },
{ id: 3, name: 'Keyboard', category: 'Electronics', price: 75 },
{ id: 4, name: 'Buku Resep', category: 'Books', price: 30 },
{ id: 5, name: 'Kopi Bubuk', category: 'Food', price: 15 },
];
function ProductItem({ product }: { product: typeof allProducts[0] }) {
// Komponen ini akan di-render ulang setiap kali ProductList di-render ulang
// kecuali jika ProductList di-memoize dengan React.memo dan props-nya stabil
console.log(`ProductItem ${product.name} rendered`);
return (
<div style={{ border: '1px solid #eee', margin: '5px', padding: '5px' }}>
<h3>{product.name}</h3>
<p>Category: {product.category}</p>
<p>Price: ${product.price}</p>
</div>
);
}
function ProductListTraditional() {
const [searchTerm, setSearchTerm] = useState('');
const [filterCategory, setFilterCategory] = useState('All');
// useMemo untuk mengoptimalkan filteredProducts
const filteredProducts = useMemo(() => {
console.log('Filtering products (traditional)');
return allProducts.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = filterCategory === 'All' || product.category === filterCategory;
return matchesSearch && matchesCategory;
});
}, [searchTerm, filterCategory]); // Dependensi manual
console.log('ProductListTraditional rendered');
return (
<div>
<h2>Product List (Traditional React)</h2>
<input
type="text"
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
>
<option value="All">All Categories</option>
<option value="Electronics">Electronics</option>
<option value="Books">Books</option>
<option value="Food">Food</option>
</select>
<div>
{filteredProducts.map(product => (
<ProductItem key={product.id} product={product} />
))}
</div>
</div>
);
}
export default ProductListTraditional;
Dalam contoh di atas, setiap kali searchTerm atau filterCategory berubah, ProductListTraditional akan di-render ulang, dan semua ProductItem juga akan di-render ulang kecuali jika ProductItem di-memoize secara eksplisit (yang mana belum kita lakukan di sini). Bahkan dengan useMemo untuk filteredProducts, komponen ProductListTraditional tetap di-render ulang.
Mengadopsi Signals untuk Komponen Pencarian
Sekarang, mari kita refactor menggunakan Signals. Kita akan memindahkan state pencarian dan filter ke luar komponen React, menjadikannya Signals.
// src/store/productFilters.ts
import { signal, computed } from '@preact/signals-react';
const allProducts = [
{ id: 1, name: 'Laptop', category: 'Electronics', price: 1200 },
{ id: 2, name: 'Mouse', category: 'Electronics', price: 25 },
{ id: 3, name: 'Keyboard', category: 'Electronics', price: 75 },
{ id: 4, name: 'Buku Resep', category: 'Books', price: 30 },
{ id: 5, name: 'Kopi Bubuk', category: 'Food', price: 15 },
];
export const searchTerm = signal('');
export const filterCategory = signal('All');
export const filteredProducts = computed(() => {
console.log('Filtering products (signals)'); // Hanya log ini saat searchTerm atau filterCategory berubah
return allProducts.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.value.toLowerCase());
const matchesCategory = filterCategory.value === 'All' || product.category === filterCategory.value;
return matchesSearch && matchesCategory;
});
});
Dan komponen React-nya:
// src/components/ProductListSignals.tsx
import React from 'react';
import { searchTerm, filterCategory, filteredProducts } from '../store/productFilters';
// Komponen ProductItem bisa tetap sama atau di-memoize jika perlu
function ProductItem({ product }: { product: typeof allProducts[0] }) {
console.log(`ProductItem ${product.name} rendered`);
return (
<div style={{ border: '1px solid #eee', margin: '5px', padding: '5px' }}>
<h3>{product.name}</h3>
<p>Category: {product.category}</p>
<p>Price: ${product.price}</p>
</div>
);
}
function ProductListSignals() {
console.log('ProductListSignals rendered'); // Log ini hanya akan muncul sekali, kecuali state lokal berubah
return (
<div>
<h2>Product List (Signals React)</h2>
<input
type="text"
placeholder