Proyek Solid menyatukan semua yang sudah kamu latih satu per satu: komponen, props, signal, memo, Show, For, dan event, jadi satu aplikasi yang dipasang ke halaman lewat satu pintu, src/index.jsx. Bayangin hari pembukaan Toko Buku Kak Nadia. Rak, label harga, mesin kasir, dan buku titipan sudah disiapkan sendiri-sendiri. Hari ini semuanya ditata jadi satu toko, lalu pintu depannya dibuka sekali. Sesudah itu, yang berubah cuma bagian kecil: angka di mesin kasir, satu baris di daftar titipan.
🧪 Tebakan cepat:
Apptoko menulisconsole.log("App jalan")di badannya. Kamu menambah tiga buku ke keranjang, menghapus satu, lalu mengetikbumidi kotak cari. Berapa kaliApp jalantercetak? Jawabannya ada di bagian "App jalan sekali".
Struktur proyek
Proyek Solid biasa dibuat dengan Vite: npm create vite@latest toko-buku -- --template solid. Isinya kira-kira:
toko-buku/
├── index.html ← punya <div id="root"></div>
├── package.json
├── vite.config.js
└── src/
├── index.jsx ← pintu masuk: render(...)
├── App.jsx
└── components/
├── Header.jsx
└── KartuBuku.jsx
src/index.jsx(atauindex.tsxdi proyek TypeScript) adalah file entry point: file pertama yang dijalankan.App.jsxmemegang data dan menyusun komponen lain. Komponen kecil tinggal di foldercomponents/.
index.jsx: render dan dispose
// src/index.jsx
import { render } from "solid-js/web"
import App from "./App"
const dispose = render(() => <App />, document.getElementById("root"))
renderdiimpor dari"solid-js/web", bukan dari"solid-js". Dia menerima fungsi yang mengembalikan<App />, dan elemen tempat memasangnya.rendermengembalikan dispose function. Memanggildispose()membongkar seluruh aplikasi: semua efek berhenti, semuaonCleanupjalan. Diuji: sesudahdispose(), isi#rootjadi kosong.
App toko: semua konsep di satu tempat
// src/App.jsx
import { createSignal, createMemo, For, Show } from "solid-js"
const katalog = [
{ id: 1, judul: "Laskar Pelangi", harga: 89000, stok: 4 },
{ id: 2, judul: "Bumi Manusia", harga: 120000, stok: 2 },
{ id: 3, judul: "Cantik Itu Luka", harga: 95000, stok: 0 },
]
const rupiah = (n) => "Rp" + n.toLocaleString("id-ID")
function KartuBuku(props) {
return (
<li>
{props.buku.judul} · {rupiah(props.buku.harga)}
<Show when={props.buku.stok > 0} fallback={<span> · Habis</span>}>
<button onClick={() => props.onTambah(props.buku)}>Tambah</button>
</Show>
</li>
)
}
export default function App() {
console.log("App jalan")
const [cari, setCari] = createSignal("")
const [keranjang, setKeranjang] = createSignal([])
const hasil = createMemo(() => katalog.filter((b) => b.judul.toLowerCase().includes(cari().toLowerCase())))
const total = createMemo(() => keranjang().reduce((t, b) => t + b.harga, 0))
const tambah = (buku) => setKeranjang((k) => [...k, { ...buku, kode: Date.now() + Math.random() }])
const hapus = (kode) => setKeranjang((k) => k.filter((b) => b.kode !== kode))
return (
<div>
<p>Keranjang: {keranjang().length} buku · {rupiah(total())}</p>
<input placeholder="Cari judul" onInput={(e) => setCari(e.target.value)} />
<ul>
<For each={hasil()}>{(b) => <KartuBuku buku={b} onTambah={tambah} />}</For>
</ul>
<ul>
<For each={keranjang()}>
{(b) => <li>{b.judul} <button onClick={() => hapus(b.kode)}>Hapus</button></li>}
</For>
</ul>
</div>
)
}
| Bagian | Alat | Dari pelajaran |
|---|---|---|
| isi kotak cari, isi keranjang | createSignal |
Signals |
| hasil pencarian, total harga | createMemo |
Memos |
| daftar buku, baris keranjang | For |
List Rendering |
tulisan Habis untuk stok 0 |
Show |
Conditional Rendering |
KartuBuku menerima buku dan onTambah |
props | Props |
- Tambah dua kali Laskar Pelangi dan sekali Bumi Manusia:
Keranjang: 3 buku · Rp298.000. Hapus satu Laskar Pelangi:Keranjang: 2 buku · Rp209.000. - Menghapus berarti menyusun array baru tanpa item itu:
k.filter((b) => b.kode !== kode). Barisnya benar-benar hilang dari daftar. totalditurunkan darikeranjangdengancreateMemo, gak disimpan di signal terpisah, jadi selalu cocok sesudah tambah atau hapus.
Form: titip judul yang belum ada
Pembeli boleh menitip judul yang belum dijual. Ini pola kedua latihan:
const [titipan, setTitipan] = createSignal([])
const [judulTitip, setJudulTitip] = createSignal("")
const jumlahTitipan = createMemo(() => titipan().length)
const titip = (e) => {
e.preventDefault()
if (!judulTitip().trim()) return
setTitipan((t) => [...t, { id: Date.now(), judul: judulTitip() }])
setJudulTitip("")
}
<form onSubmit={titip}>
<input value={judulTitip()} onInput={(e) => setJudulTitip(e.target.value)} />
<button type="submit">Titip</button>
</form>
<p>{jumlahTitipan()} judul dititip</p>
<ul>
<For each={titipan()}>{(t) => <li>{t.judul}</li>}</For>
</ul>
<form onSubmit={...}>menangkap klik tomboltype="submit"dan tombol Enter.e.preventDefault()mencegah halaman dimuat ulang.if (!judulTitip().trim()) return: kotak kosong atau cuma spasi gak menambah baris.value={judulTitip()}mengikat kotaknya ke signal, jadisetJudulTitip("")benar-benar mengosongkan kotak sesudah pencatatan.- Latihan 1 (Todo App) memakai pola ini plus checkbox:
setTodos((t) => t.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo))), dan jumlah yang belum selesai lewatcreateMemo(() => todos().filter((t) => !t.done).length). Latihan 2 (Shopping List) memakai pola ini persis, dengan<p>Total: {total()} item</p>. Di kedua latihan, tombol pencatatnya bertulisanTambah, dan tiap baris punya tombolHapus.
App jalan sekali
App jalan tercetak sekali, saat halaman dibuka. Itu jawaban tebakannya. Diuji di Solid 1.9: tiga kali Tambah, satu Hapus, dan mengetik di kotak cari gak menjalankan App lagi. Yang jalan ulang cuma bagian yang membaca signal yang berubah: total dan teks keranjang saat keranjang berubah, hasil dan daftar buku saat cari berubah.
⚠️ Jebakan yang sering ketemu
- Membongkar props di parameter.
function Header({ jumlah, total })membaca nilainya sekali. Diuji: sesudah tiga kali Tambah, header tetapKeranjang: 0 buku · Rp0. Pakaiprops.jumlah. - Lupa
e.preventDefault(). Form mengirim dan memuat ulang halaman, jadi daftarnya hilang. - Menyembunyikan baris alih-alih menghapusnya. Latihan memeriksa
<li>benar-benar hilang dari<ul>sesudah Hapus, dan kodemu memuatfilter(. - Lupa mengosongkan atau mengabaikan isian kosong. Latihan memeriksa kotaknya kosong sesudah Tambah, dan Tambah dengan kotak kosong gak membuat
<li>.
TL;DR: Proyek Solid dimulai dari src/index.jsx (atau index.tsx): render(() => <App />, document.getElementById("root")) dari "solid-js/web", yang mengembalikan dispose function untuk membongkar aplikasinya. App menyusun semuanya: createSignal untuk isian dan daftar, createMemo untuk turunan seperti total dan jumlah, For untuk daftar, Show untuk syarat, props untuk komponen kecil. Form memakai onSubmit + e.preventDefault(), mengabaikan isian kosong, menambah dengan [...lama, baru], mengosongkan kotaknya, dan menghapus dengan filter. App jalan sekali; sesudahnya cuma bagian yang membaca signal yang berubah yang diperbarui.