1. Pengenalan SolidJS
SolidJS adalah framework JavaScript yang dirancang untuk membangun antarmuka pengguna (UI) dengan pendekatan fine-grained reactivity. Dikembangkan oleh Ryan Carniato dan pertama kali dirilis pada tahun 2021, SolidJS menawarkan performa yang luar biasa dengan sintaksis yang familiar bagi pengguna React.
Berbeda dengan React yang menggunakan Virtual DOM, SolidJS langsung memanipulasi DOM asli. Setiap perubahan state di SolidJS hanya memperbarui elemen DOM spesifik yang terpengaruh — tidak me-render ulang seluruh component. Hasilnya? Performa yang sangat cepat dan memory footprint yang kecil.
Mengapa Memilih SolidJS?
| Keunggulan | Penjelasan |
|---|---|
| Tanpa Virtual DOM | SolidJS langsung memanipulasi DOM — update hanya pada elemen yang berubah, bukan seluruh component tree |
| True Reactivity | Sistem reaksi berbasis dependency tracking otomatis — tidak perlu useEffect atau dependency array |
| Sintaksis Mirip React | Menggunakan JSX dan hooks-like API (createSignal), sehingga mudah dipelajari oleh React developer |
| Bundle Kecil | Runtime hanya ~7KB (gzip) — jauh lebih kecil dari React (42KB) atau Angular (143KB) |
| Performa Terbaik | Secara konsisten menempati peringkat teratas di benchmark JS Frameworks |
| TypeScript Support | Dibangun dengan TypeScript dari awal, dengan type inference yang sangat baik |
Bagaimana SolidJS Bekerja?
┌─────────────────────────────────────────────────────────┐
│ SIGNAL-BASED REACTIVITY │
│ │
│ ┌──────────────┐ │
│ │ createSignal │──► Signal (state) │
│ │ (count, 0) │ │
│ └──────┬───────┘ │
│ │ │
│ │ getter (dipanggil di template) │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Template / JSX │ │
│ │ │ │
│ │ <p>Count: {count()}</p> ← DOM Node 1 │ │
│ │ <p>Double: {double()}</p> ← DOM Node 2 │ │
│ │ <button onClick={inc}>+</button> │ │
│ └──────────────────────────────────────────────────┘ │
│ ▲ │
│ │ (hanya node yang terpengaruh yang di-update) │
│ │ │
│ ┌──────┴───────┐ │
│ │ Signal │──► setter (setCount) │
│ │ Updated! │──► Re-run tracked effects │
│ └──────────────┘──► Update DOM nodes yang subscribed │
│ │
│ Perbandingan: │
│ React: setState → Re-render component → Diff DOM │
│ Solid: setSignal → Update tracked DOM nodes langsung │
└─────────────────────────────────────────────────────────┘
Fine-grained reativity berarti sistem reaksi SolidJS melacak dependency pada level yang sangat detail — hingga ke individual text node di DOM. Saat sebuah signal berubah, hanya node DOM yang menggunakan signal tersebut yang diperbarui. Tidak ada re-render component, tidak ada diffing, tidak ada overhead Virtual DOM.
2. Setup dan Instalasi
SolidJS dapat di-setup dengan sangat mudah menggunakan template yang disediakan oleh tim SolidJS. Kita bisa menggunakan Vite sebagai bundler untuk development yang cepat.
Memulai Proyek Baru
# Buat proyek SolidJS baru dengan template TypeScript npx degit solidjs/templates/ts my-solid-app # Atau menggunakan JavaScript npx degit solidjs/templates/js my-solid-app # Masuk ke direktori cd my-solid-app # Instal dependencies npm install # Jalankan development server npm run dev # Build untuk production npm run build
Struktur Folder
my-solid-app/ ├── node_modules/ ├── public/ │ └── favicon.ico ├── src/ │ ├── assets/ # Static files │ ├── components/ # Komponen reusable │ │ ├── Counter.tsx │ │ ├── TodoList.tsx │ │ └── UserCard.tsx │ ├── pages/ # Halaman │ │ ├── Home.tsx │ │ ├── About.tsx │ │ └── NotFound.tsx │ ├── stores/ # State management │ │ └── todoStore.ts │ ├── App.tsx # Root component │ ├── App.css # Global styles │ ├── index.tsx # Entry point │ └── index.css # Base styles ├── index.html # HTML template ├── package.json ├── tsconfig.json ├── vite.config.ts └── README.md
Entry Point
// index.tsx — Entry point SolidJS
import { render } from 'solid-js/web';
import App from './App';
// render() mirip ReactDOM.render() di React
// Mount App component ke elemen #root di HTML
render(() => <App />, document.getElementById('root')!);
3. Signals: Reactive Primitives
Signals adalah fondasi reactivity di SolidJS. Mirip dengan useState di React, tetapi dengan perbedaan mendasar: Signals tidak memicu re-render component. Sebaliknya, Signals langsung memperbarui DOM nodes yang spesifik.
createSignal — Dasar State di SolidJS
import { createSignal } from 'solid-js';
function Counter() {
// createSignal mengembalikan [getter, setter]
const [count, setCount] = createSignal(0);
// count() — panggil sebagai fungsi untuk mendapatkan nilai
// setCount(5) — set nilai baru
// setCount(prev => prev + 1) — update berdasarkan nilai sebelumnya
return (
<div>
<h2>Count: {count()}</h2>
<button onClick={() => setCount(count() + 1)}>Tambah</button>
<button onClick={() => setCount(count() - 1)}>Kurang</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Di SolidJS, signal diakses dengan memanggilnya sebagai fungsi: count(), bukan langsung count. Ini karena SolidJS perlu melacak akses signal untuk membuat dependency tracking. Jika kamu lupa memanggil (), dependency tracking tidak akan bekerja dan UI tidak akan update.
createSignal dengan Tipe Data Berbeda
import { createSignal } from 'solid-js';
function DataTypesDemo() {
// Signal dengan number
const [count, setCount] = createSignal<number>(0);
// Signal dengan string
const [name, setName] = createSignal<string>('Guest');
// Signal dengan boolean
const [isOpen, setIsOpen] = createSignal<boolean>(false);
// Signal dengan object
const [user, setUser] = createSignal<{ name: string; age: number }>({
name: 'Budi',
age: 25
});
// Signal dengan array
const [items, setItems] = createSignal<string[]>(['apel', 'mangga']);
// Update object (gunakan spread untuk merge)
const updateUserName = () => {
setUser(prev => ({ ...prev, name: 'Andi' }));
};
// Update array
const addItem = (item: string) => {
setItems(prev => [...prev, item]);
};
const removeItem = (index: number) => {
setItems(prev => prev.filter((_, i) => i !== index));
};
return (
<div>
<p>Count: {count()}</p>
<p>Nama: {name()}</p>
<p>Terbuka: {isOpen() ? 'Ya' : 'Tidak'}</p>
<p>User: {user().name}, Umur: {user().age}</p>
<p>Items: {items().join(', ')}</p>
</div>
);
}
createSignal vs React useState
| Aspek | SolidJS createSignal | React useState |
|---|---|---|
| Akses Nilai | count() — dipanggil sebagai fungsi | count — langsung diakses |
| Re-render | Tidak ada re-render — hanya update DOM spesifik | Seluruh component re-render |
| Fine-grained | Ya — dependency tracking otomatis per expression | Tidak — re-render keseluruhan component |
| Setter | setCount(5) atau setCount(p => p+1) | setCount(5) atau setCount(p => p+1) |
| Lazy Init | createSignal(() => heavyComputation()) | useState(() => heavyComputation()) |
4. Components di SolidJS
Components di SolidJS hanya berjalan sekali — saat pertama kali dibuat. Setelah itu, hanya reactive expressions (signals) di dalam template yang di-update. Ini berbeda dengan React di mana seluruh function component berjalan ulang setiap render.
Anatomi Component
// components/UserCard.tsx
import { Component, Show, createSignal } from 'solid-js';
// Props interface
interface UserCardProps {
name: string;
email: string;
avatar: string;
role?: string;
onDelete?: (name: string) => void;
}
// Component sebagai arrow function dengan tipe Component
const UserCard: Component<UserCardProps> = (props) => {
const [isExpanded, setIsExpanded] = createSignal(false);
// Component function ini HANYA berjalan sekali!
console.log('UserCard created:', props.name);
// Props di SolidJS bersifat reactive — mirip signals
// Bisa dipanggil: props.name (object property, bukan function)
const badgeColor = () => {
const colors: Record<string, string> = {
'Admin': '#e74c3c',
'Editor': '#f39c12',
'User': '#2ecc71'
};
return colors[props.role || 'User'] || '#95a5a6';
};
return (
<div class="user-card" classList={{ expanded: isExpanded() }}>
<div class="user-header">
<img src={props.avatar} alt={props.name} class="user-avatar" />
<div class="user-info">
<h3>{props.name}</h3>
<p>{props.email}</p>
<span
class="badge"
style={{ "background-color": badgeColor() }}
>
{props.role || 'User'}
</span>
</div>
</div>
<div class="user-actions">
<button onClick={() => setIsExpanded(!isExpanded())}>
{isExpanded() ? 'Tutup' : 'Detail'}
</button>
<button onClick={() => props.onDelete?.(props.name)}>
🗑️ Hapus
</button>
</div>
<Show when={isExpanded()}>
<div class="user-details">
<p>Detail lengkap tentang {props.name}</p>
<p>Role: {props.role || 'User'}</p>
</div>
</Show>
</div>
);
};
export default UserCard;
// === Parent Component ===
// components/UserList.tsx
import { Component, For } from 'solid-js';
import UserCard from './UserCard';
const UserList: Component = () => {
const [users, setUsers] = createSignal([
{ name: 'Budi', email: 'budi@mail.com', avatar: '/img/budi.jpg', role: 'Admin' },
{ name: 'Ani', email: 'ani@mail.com', avatar: '/img/ani.jpg', role: 'Editor' },
{ name: 'Citra', email: 'citra@mail.com', avatar: '/img/citra.jpg', role: 'User' }
]);
const deleteUser = (name: string) => {
setUsers(prev => prev.filter(u => u.name !== name));
};
return (
<div class="user-list">
<h2>Daftar User ({users().length})</h2>
<For each={users()}>
{(user) => (
<UserCard
name={user.name}
email={user.email}
avatar={user.avatar}
role={user.role}
onDelete={deleteUser}
/>
)}
</For>
</div>
);
};
export default UserList;
Props di SolidJS — Reactive dan Getter-based
import { Component, mergeProps } from 'solid-js';
// Props di SolidJS bersifat reactive secara default
// Props diakses sebagai getter (property access), bukan function call
const Greeting: Component<{ name: string; greeting?: string }> = (props) => {
// props.name akan otomatis update jika parent mengubah nama
// props.greeting memiliki nilai default
// Gunakan mergeProps untuk default values
const merged = mergeProps({ greeting: 'Halo' }, props);
return (
<div>
<h1>{merged.greeting}, {merged.name}!</h1>
</div>
);
};
// Destructuring props — HATI-HATI!
// Jangan destructure langsung karena akan memutus reactivity
// ❌ SALAH:
// const Bad: Component<Props> = ({ name }) => <h1>{name}</h1>;
// ✅ BENAR:
// const Good: Component<Props> = (props) => <h1>{props.name}</h1>;
// Atau gunakan splitProps untuk membagi props
import { splitProps } from 'solid-js';
const UserAvatar: Component<UserCardProps> = (props) => {
// Pisahkan props untuk component ini vs props yang di-pass ke elemen
const [local, others] = splitProps(props, ['name', 'avatar']);
return (
<div {...others}>
<img src={local.avatar} alt={local.name} />
<span>{local.name}</span>
</div>
);
};
5. Control Flow: Show, For, Switch
SolidJS menyediakan komponen control flow khusus yang lebih efisien dibandingkan menggunakan operator JavaScript biasa di dalam JSX. Komponen ini dirancang untuk memanfaatkan fine-grained reactivity.
Show — Conditional Rendering
import { Component, createSignal, Show } from 'solid-js';
const AuthDemo: Component = () => {
const [isLoggedIn, setIsLoggedIn] = createSignal(false);
const [user, setUser] = createSignal({ name: 'Budi', role: 'Admin' });
return (
<div>
<!-- Show dengan when -->
<Show
when={isLoggedIn()}
fallback={<button onClick={() => setIsLoggedIn(true)}>Login</button>}
>
<h2>Selamat datang, {user().name}!</h2>
<button onClick={() => setIsLoggedIn(false)}>Logout</button>
</Show>
<!-- Show dengan keyed (untuk mengakses data di children) -->
<Show when={user()} keyed>
{(u) => (
<div>
<p>Nama: {u.name}</p>
<p>Role: {u.role}</p>
</div>
)}
</Show>
</div>
);
};
For — Rendering Lists
import { Component, createSignal, For, Index } from 'solid-js';
interface Product {
id: number;
name: string;
price: number;
}
const ProductList: Component = () => {
const [products, setProducts] = createSignal<Product[]>([
{ id: 1, name: 'Laptop', price: 12000000 },
{ id: 2, name: 'Mouse', price: 250000 },
{ id: 3, name: 'Keyboard', price: 750000 }
]);
const [searchTerm, setSearchTerm] = createSignal('');
// Filter products berdasarkan search
const filteredProducts = () =>
products().filter(p =>
p.name.toLowerCase().includes(searchTerm().toLowerCase())
);
const formatPrice = (price: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR'
}).format(price);
return (
<div>
<input
type="text"
placeholder="Cari produk..."
value={searchTerm()}
onInput={(e) => setSearchTerm(e.currentTarget.value)}
/>
<p>{filteredProducts().length} produk ditemukan</p>
{/* For: Untuk list yang berubah (dynamic) */}
<For each={filteredProducts()} fallback={<p>Tidak ada produk</p>}>
{(product) => (
<div class="product-card">
<h3>{product.name}</h3>
<p>{formatPrice(product.price)}</p>
<button onClick={() =>
setProducts(prev => prev.filter(p => p.id !== product.id))
}>
Hapus
</button>
</div>
)}
</For>
{/* Index: Untuk list yang isinya stabil (static) */}
<h3>Dengan Index (lebih cepat untuk list statis):</h3>
<Index each={filteredProducts()}>
{(product, index) => (
<div>
{index + 1}. {product().name} — {formatPrice(product().price)}
</div>
)}
</Index>
</div>
);
};
Switch/Match — Multiple Conditions
import { Component, createSignal, Switch, Match } from 'solid-js';
const StatusDisplay: Component = () => {
const [status, setStatus] = createSignal<'loading' | 'success' | 'error'>('loading');
return (
<div>
<div class="tabs">
<button onClick={() => setStatus('loading')}>Loading</button>
<button onClick={() => setStatus('success')}>Success</button>
<button onClick={() => setStatus('error')}>Error</button>
</div>
<Switch fallback={<p>Status tidak diketahui</p>}>
<Match when={status() === 'loading'}>
<div class="spinner">
<p>⏳ Memuat data...</p>
</div>
</Match>
<Match when={status() === 'success'}>
<div class="success">
<p>✅ Data berhasil dimuat!</p>
</div>
</Match>
<Match when={status() === 'error'}>
<div class="error">
<p>❌ Terjadi kesalahan saat memuat data</p>
<button onClick={() => setStatus('loading')}>Coba Lagi</button>
</div>
</Match>
</Switch>
</div>
);
};
6. Effects dan Memos
SolidJS menyediakan beberapa reactive primitives untuk menangani side effects dan komputasi derived: createEffect, createMemo, dan createComputed.
createEffect — Side Effects
import { createSignal, createEffect, onCleanup } from 'solid-js';
function EffectDemo() {
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('Budi');
// createEffect: Berjalan otomatis saat signal yang diakses berubah
createEffect(() => {
console.log('Count berubah menjadi:', count());
});
// Effect dengan dependency tracking otomatis
// Hanya berjalan saat name() berubah, karena hanya name() yang diakses
createEffect(() => {
console.log(`Halo, ${name()}!`);
document.title = `Hello ${name()} - My App`;
});
// Effect dengan cleanup (mirip useEffect return function)
createEffect(() => {
const timer = setInterval(() => {
console.log('Timer berjalan untuk:', name());
}, 1000);
// Cleanup function — dijalankan saat effect re-run atau di-destroy
onCleanup(() => {
clearInterval(timer);
console.log('Timer dibersihkan untuk:', name());
});
});
return (
<div>
<p>Count: {count()}</p>
<button onClick={() => setCount(count() + 1)}>+</button>
<input value={name()} onInput={e => setName(e.currentTarget.value)} />
</div>
);
}
createMemo — Derived/Computed Values
import { createSignal, createMemo } from 'solid-js';
function MemoDemo() {
const [items, setItems] = createSignal([
{ name: 'Apel', price: 5000, quantity: 3 },
{ name: 'Mangga', price: 8000, quantity: 2 },
{ name: 'Jeruk', price: 6000, quantity: 5 }
]);
const [discount, setDiscount] = createSignal(0.1); // 10%
// createMemo: Derived value yang di-cache
// Hanya re-compute saat dependency berubah
const subtotal = createMemo(() =>
items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
const discountAmount = createMemo(() =>
subtotal() * discount()
);
const total = createMemo(() =>
subtotal() - discountAmount()
);
const itemCount = createMemo(() =>
items().reduce((sum, item) => sum + item.quantity, 0)
);
const formatRupiah = (amount: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR'
}).format(amount);
const addRandomItem = () => {
const names = ['Pisang', 'Durian', 'Salak', 'Rambutan'];
const randomName = names[Math.floor(Math.random() * names.length)];
const randomPrice = Math.floor(Math.random() * 10000) + 2000;
setItems(prev => [...prev, {
name: randomName,
price: randomPrice,
quantity: 1
}]);
};
return (
<div>
<h2>Keranjang Belanja ({itemCount()} item)</h2>
<For each={items()}>
{(item, i) => (
<div>
{item.name}: {item.quantity} × {formatRupiah(item.price)}
= {formatRupiah(item.price * item.quantity)}
</div>
)}
</For>
<hr />
<p>Subtotal: {formatRupiah(subtotal())}</p>
<p>Diskon ({(discount() * 100).toFixed(0)}%): -{formatRupiah(discountAmount())}</p>
<p><strong>Total: {formatRupiah(total())}</strong></p>
<button onClick={addRandomItem}>+ Tambah Item</button>
<input
type="range"
min="0"
max="0.5"
step="0.05"
value={discount()}
onInput={e => setDiscount(parseFloat(e.currentTarget.value))}
/>
</div>
);
}
createSignal vs createMemo vs createEffect
| Primitive | Fungsi | Kapan Digunakan |
|---|---|---|
createSignal | State dasar yang bisa di-get dan di-set | Setiap data yang berubah dari waktu ke waktu |
createMemo | Nilai derived yang di-cache dari signal lain | Kalkulasi yang mahal atau derived data |
createEffect | Side effects yang berjalan saat dependency berubah | Logging, DOM manipulation, API calls, sync data |
createComputed | Seperti createMemo tapi synchronous dan tanpa return value | Updating reactive state berdasarkan signal lain (jarang dipakai) |
7. Stores: State Management
Stores di SolidJS menyediakan cara untuk mengelola state yang kompleks dan deeply nested dengan reactivity yang tetap fine-grained. Berbeda dengan signals yang hanya reactive di level pertama, stores bisa reactive hingga level nested properties.
createStore — Deep Reactive State
// stores/todoStore.ts
import { createStore, produce } from 'solid-js/store';
interface Todo {
id: number;
text: string;
completed: boolean;
tags: string[];
}
interface TodoStore {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
stats: {
total: number;
completed: number;
active: number;
};
}
// createStore — reactive hingga nested properties
const [store, setStore] = createStore<TodoStore>({
todos: [],
filter: 'all',
stats: {
total: 0,
completed: 0,
active: 0
}
});
// === Menggunakan Store ===
// Akses nested property — reactive!
// store.todos[0].text — reactive
// store.stats.completed — reactive
// store.filter — reactive
// === Update Store ===
// Set property langsung
setStore('filter', 'active');
// Set nested property dengan path
setStore('stats', 'total', 5);
setStore('stats', 'completed', 3);
// Set dengan function updater
setStore('stats', 'active', prev => prev - 1);
// Add item ke array
setStore('todos', store.todos.length, {
id: Date.now(),
text: 'Belajar SolidJS',
completed: false,
tags: ['belajar', 'frontend']
});
// Update item spesifik dalam array
setStore('todos', (todo) => todo.id === 42, 'completed', true);
// Update nested array property
setStore('todos', (todo) => todo.id === 42, 'tags', tags =>
[...tags, 'penting']
);
// Delete item dari array
setStore('todos', todos => todos.filter(t => t.id !== 42));
// === Produce: Immutable update dengan mutable syntax ===
setStore(produce(store => {
store.todos.push({
id: Date.now(),
text: 'Item baru',
completed: false,
tags: []
});
store.stats.total++;
store.stats.active++;
}));
// Filtered todos (derived dari store)
import { createMemo } from 'solid-js';
const filteredTodos = createMemo(() => {
switch (store.filter) {
case 'active':
return store.todos.filter(t => !t.completed);
case 'completed':
return store.todos.filter(t => t.completed);
default:
return store.todos;
}
});
Store Patterns: Todo App Lengkap
// components/TodoApp.tsx
import { Component, createSignal, For, Show, createMemo } from 'solid-js';
import { createStore } from 'solid-js/store';
const TodoApp: Component = () => {
const [store, setStore] = createStore({
todos: [] as { id: number; text: string; done: boolean }[],
newTodo: ''
});
const [filter, setFilter] = createSignal<'all' | 'active' | 'done'>('all');
const filteredTodos = createMemo(() => {
switch (filter()) {
case 'active': return store.todos.filter(t => !t.done);
case 'done': return store.todos.filter(t => t.done);
default: return store.todos;
}
});
const remaining = createMemo(() =>
store.todos.filter(t => !t.done).length
);
const addTodo = () => {
if (!store.newTodo.trim()) return;
setStore('todos', todos => [
...todos,
{ id: Date.now(), text: store.newTodo.trim(), done: false }
]);
setStore('newTodo', '');
};
const toggleTodo = (id: number) => {
setStore('todos', t => t.id === id, 'done', d => !d);
};
const removeTodo = (id: number) => {
setStore('todos', todos => todos.filter(t => t.id !== id));
};
return (
<div class="todo-app">
<h1>📋 Todo List</h1>
<div class="add-todo">
<input
value={store.newTodo}
onInput={e => setStore('newTodo', e.currentTarget.value)}
onKeyDown={e => e.key === 'Enter' && addTodo()}
placeholder="Tambah todo baru..."
/>
<button onClick={addTodo}>Tambah</button>
</div>
<div class="filters">
<button classList={{ active: filter() === 'all' }}
onClick={() => setFilter('all')}>Semua</button>
<button classList={{ active: filter() === 'active' }}
onClick={() => setFilter('active')}>Aktif</button>
<button classList={{ active: filter() === 'done' }}
onClick={() => setFilter('done')}>Selesai</button>
</div>
<ul class="todo-list">
<For each={filteredTodos()} fallback={<p>Tidak ada todo</p>}>
{(todo) => (
<li classList={{ done: todo.done }}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>✕</button>
</li>
)}
</For>
</ul>
<p class="remaining">{remaining()} item tersisa</p>
</div>
);
};
export default TodoApp;
8. Lifecycle dan Context
Di SolidJS, lifecycle ditangani melalui onMount, onCleanup, dan createEffect. Untuk berbagi data antar component tanpa prop drilling, SolidJS menggunakan Context.
Lifecycle Hooks
import { Component, onMount, onCleanup, createSignal, createEffect } from 'solid-js';
const LifecycleDemo: Component = () => {
const [data, setData] = createSignal(null);
const [windowWidth, setWindowWidth] = createSignal(window.innerWidth);
// onMount: Berjalan sekali setelah component pertama kali mount ke DOM
// Mirip componentDidMount di React atau ngOnInit di Angular
onMount(async () => {
console.log('Component mounted!');
// Fetch data saat mount
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Gagal fetch data:', error);
}
});
// onCleanup: Berjalan saat component di-destroy dari DOM
// Mirip componentWillUnmount atau ngOnDestroy
onMount(() => {
const handleResize = () => setWindowWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
onCleanup(() => {
window.removeEventListener('resize', handleResize);
console.log('Event listener dibersihkan');
});
});
// createEffect: Berjalan otomatis saat signal berubah
// Dependency tracking OTOMATIS
createEffect(() => {
console.log('Window width changed:', windowWidth());
});
return (
<div>
<p>Lebar window: {windowWidth()}px</p>
<Show when={data()} fallback={<p>Loading...</p>}>
<pre>{JSON.stringify(data(), null, 2)}</pre>
</Show>
</div>
);
};
Context — Berbagi State Tanpa Prop Drilling
// context/ThemeContext.tsx
import { createContext, useContext, createSignal, ParentComponent } from 'solid-js';
interface ThemeContextType {
theme: () => string;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType>();
// Provider component
export const ThemeProvider: ParentComponent = (props) => {
const [theme, setTheme] = createSignal('dark');
const toggleTheme = () => {
setTheme(prev => prev === 'dark' ? 'light' : 'dark');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{props.children}
</ThemeContext.Provider>
);
};
// Custom hook untuk menggunakan context
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme harus digunakan di dalam ThemeProvider');
}
return context;
}
// === Penggunaan ===
// App.tsx
const App: Component = () => (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
// Header.tsx — bisa akses theme tanpa prop drilling
const Header: Component = () => {
const { theme, toggleTheme } = useTheme();
return (
<header class={theme() === 'dark' ? 'dark-header' : 'light-header'}>
<h1>Aplikasi Saya</h1>
<button onClick={toggleTheme}>
{theme() === 'dark' ? '☀️' : '🌙'}
</button>
</header>
);
};
9. Routing dengan Solid Router
@solidjs/router adalah router resmi untuk SolidJS yang mendukung file-based dan code-based routing, nested routes, lazy loading, dan data loading.
Konfigurasi Router
# Instal Solid Router npm install @solidjs/router
// App.tsx
import { Component, lazy } from 'solid-js';
import { Router, Route, Routes, A, useNavigate } from '@solidjs/router';
// Lazy loading pages
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const About = lazy(() => import('./pages/About'));
const NotFound = lazy(() => import('./pages/NotFound'));
const App: Component = () => {
return (
<Router>
{/* Navbar */}
<nav>
<A href="/" activeClass="active">Beranda</A>
<A href="/products" activeClass="active">Produk</A>
<A href="/about" activeClass="active">Tentang</A>
</nav>
{/* Routes */}
<Routes>
<Route path="/" component={Home} />
<Route path="/products" component={Products} />
<Route path="/products/:id" component={ProductDetail} />
<Route path="/about" component={About} />
<Route path="*" component={NotFound} />
</Routes>
</Router>
);
};
export default App;
// === pages/ProductDetail.tsx ===
import { Component, createResource } from 'solid-js';
import { useParams, useNavigate } from '@solidjs/router';
const ProductDetail: Component = () => {
const params = useParams(); // Access URL params
const navigate = useNavigate(); // Programmatic navigation
// Fetch data berdasarkan route param
const fetchProduct = async (id: string) => {
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
};
const [product] = createResource(() => params.id, fetchProduct);
return (
<div>
<Show when={product()} fallback={<p>Loading...</p>}>
<h1>{product()!.name}</h1>
<p>Harga: Rp {product()!.price.toLocaleString()}</p>
<button onClick={() => navigate('/products')}>
← Kembali ke Produk
</button>
</Show>
</div>
);
};
10. SolidJS vs React vs Vue
| Aspek | SolidJS | React | Vue.js |
|---|---|---|---|
| Re-render | Tidak ada — fine-grained DOM updates | Seluruh component re-render | Component-level reactivity |
| State Primitive | createSignal (getter function) | useState (variable) | ref() / reactive() |
| Bundle Size | ~7 KB (gzip) | ~42 KB (gzip) | ~33 KB (gzip) |
| Virtual DOM | Tidak menggunakan | Menggunakan | Menggunakan |
| Learning Curve | 🟡 Sedang (mirip React) | 🟡 Sedang | 🟢 Mudah |
| Ecosystem | 🟡 Berkembang | 🟢 Sangat besar | 🟢 Besar |
| SSR | SolidStart | Next.js | Nuxt.js |
| Performa Benchmark | 🥇 Tercepat | 🥉 Sedang | 🥈 Cepat |
| Job Market | 🔴 Terbatas | 🟢 Sangat besar | 🟢 Besar |
11. Best Practices
| Praktik | Penjelasan |
|---|---|
| Jangan Destructure Props | Selalu akses props sebagai props.name, bukan { name } agar reactivity tetap bekerja |
| Gunakan For untuk List | Hindari .map() — gunakan <For> untuk list dynamic dan <Index> untuk list statis |
| createMemo untuk Komputasi | Jangan letakkan kalkulasi mahal di template — gunakan createMemo() |
| Stores untuk State Kompleks | Gunakan createStore untuk state deeply nested, createSignal untuk state sederhana |
| Lazy Load Routes | Gunakan lazy() untuk memuat pages hanya saat dibutuhkan |
| Panggil Signal sebagai Fungsi | Selalu gunakan count() bukan count untuk mengakses signal |
12. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang SolidJS: