Web Development

SolidJS: Reactive UI Framework

Tutorial lengkap belajar SolidJS dari nol — signals, components, stores, reactive primitives, dan membangun UI yang ultra-performant tanpa virtual DOM

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 DOMSolidJS langsung memanipulasi DOM — update hanya pada elemen yang berubah, bukan seluruh component tree
True ReactivitySistem reaksi berbasis dependency tracking otomatis — tidak perlu useEffect atau dependency array
Sintaksis Mirip ReactMenggunakan JSX dan hooks-like API (createSignal), sehingga mudah dipelajari oleh React developer
Bundle KecilRuntime hanya ~7KB (gzip) — jauh lebih kecil dari React (42KB) atau Angular (143KB)
Performa TerbaikSecara konsisten menempati peringkat teratas di benchmark JS Frameworks
TypeScript SupportDibangun dengan TypeScript dari awal, dengan type inference yang sangat baik

Bagaimana SolidJS Bekerja?

Diagram: SolidJS Fine-Grained Reactivity
┌─────────────────────────────────────────────────────────┐
│              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   │
└─────────────────────────────────────────────────────────┘
💡 Apa itu Fine-Grained Reactivity?

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

bash
# 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

text
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

typescript
// 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

typescript
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>
  );
}
⚠️ Penting: Panggil Signal sebagai Fungsi!

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

typescript
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 Nilaicount() — dipanggil sebagai fungsicount — langsung diakses
Re-renderTidak ada re-render — hanya update DOM spesifikSeluruh component re-render
Fine-grainedYa — dependency tracking otomatis per expressionTidak — re-render keseluruhan component
SettersetCount(5) atau setCount(p => p+1)setCount(5) atau setCount(p => p+1)
Lazy InitcreateSignal(() => 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

typescript
// 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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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
createSignalState dasar yang bisa di-get dan di-setSetiap data yang berubah dari waktu ke waktu
createMemoNilai derived yang di-cache dari signal lainKalkulasi yang mahal atau derived data
createEffectSide effects yang berjalan saat dependency berubahLogging, DOM manipulation, API calls, sync data
createComputedSeperti createMemo tapi synchronous dan tanpa return valueUpdating 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

typescript
// 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

typescript
// 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

typescript
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

typescript
// 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

bash
# Instal Solid Router
npm install @solidjs/router
typescript
// 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-renderTidak ada — fine-grained DOM updatesSeluruh component re-renderComponent-level reactivity
State PrimitivecreateSignal (getter function)useState (variable)ref() / reactive()
Bundle Size~7 KB (gzip)~42 KB (gzip)~33 KB (gzip)
Virtual DOMTidak menggunakanMenggunakanMenggunakan
Learning Curve🟡 Sedang (mirip React)🟡 Sedang🟢 Mudah
Ecosystem🟡 Berkembang🟢 Sangat besar🟢 Besar
SSRSolidStartNext.jsNuxt.js
Performa Benchmark🥇 Tercepat🥉 Sedang🥈 Cepat
Job Market🔴 Terbatas🟢 Sangat besar🟢 Besar

11. Best Practices

Praktik Penjelasan
Jangan Destructure PropsSelalu akses props sebagai props.name, bukan { name } agar reactivity tetap bekerja
Gunakan For untuk ListHindari .map() — gunakan <For> untuk list dynamic dan <Index> untuk list statis
createMemo untuk KomputasiJangan letakkan kalkulasi mahal di template — gunakan createMemo()
Stores untuk State KompleksGunakan createStore untuk state deeply nested, createSignal untuk state sederhana
Lazy Load RoutesGunakan lazy() untuk memuat pages hanya saat dibutuhkan
Panggil Signal sebagai FungsiSelalu 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:

Pertanyaan 1: Apa primitive dasar untuk state management di SolidJS?

a) useState
b) createSignal
c) reactive()
d) ref()

Pertanyaan 2: Mengapa SolidJS TIDAK menggunakan Virtual DOM?

a) Karena terlalu kompleks untuk diimplementasikan
b) Karena fine-grained reactivity memungkinkan update DOM secara langsung dan spesifik
c) Karena SolidJS hanya untuk aplikasi kecil
d) Karena SolidJS menggunakan Shadow DOM

Pertanyaan 3: Komponen apa yang digunakan untuk conditional rendering di SolidJS?

a) *ngIf
b) v-if
c) Show
d) Conditional

Pertanyaan 4: Apa perbedaan antara <For> dan <Index>?

a) Tidak ada perbedaan
b) For untuk list yang items berubah, Index untuk list yang index-based dan stabil
c) For lebih cepat dari Index
d) Index hanya untuk angka

Pertanyaan 5: Kapan sebaiknya menggunakan createStore dibanding createSignal?

a) Selalu gunakan createStore
b) createStore untuk state sederhana, createSignal untuk state kompleks
c) createStore untuk state deeply nested dan kompleks, createSignal untuk state sederhana/primitif
d) createSignal sudah deprecated, harus pakai createStore
🔍 Zoom
100%
🎨 Tema