Referensi cepat Solid.js. Signals, effects, memos, components, context, dan control flow. Perfect buat developer React yang mau coba reactivity tanpa virtual DOM.
# Vite template (recommended)
npm create vite@latest my-app -- --template solid-ts
# atau degit (lebih cepat)
npx degit solidjs/templates/ts my-app
cd my-app && npm install
# SolidStart (full-stack, kayak Next.js untuk Solid)
npm create solid@latestVerifikasi instalasi:
npm run devSignal adalah unit reactivity dasar di Solid. Mirip useState di React, tapi lebih granular.
import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
return (
<button onClick={() => setCount(count() + 1)}>
Count: {count()}
</button>
);
}Perbedaan penting sama React: signal adalah function. Kamu harus panggil count() buat baca nilainya.
// Solid
const [count, setCount] = createSignal(0);
console.log(count()); // 0 (panggil sebagai function)
// React
const [count, setCount] = useState(0);
console.log(count); // 0 (baca langsung)const [count, setCount] = createSignal(0);
// Set langsung
setCount(5);
// Dari nilai sebelumnya
setCount(prev => prev + 1);
// Callback dengan return false = batalkan update
setCount(prev => prev > 10 ? prev : prev + 1);const [user, setUser] = createSignal({ name: 'Galih', age: 25 });
// Update properti tertentu
setUser(prev => ({ ...prev, age: 26 }));
// Array
const [items, setItems] = createSignal([1, 2, 3]);
setItems(prev => [...prev, 4]);
setItems(prev => prev.filter(x => x !== 2));// Default: signal ngga trigger kalau nilai barunya sama (Object.is)
const [count, setCount] = createSignal(0);
setCount(0); // Tidak trigger re-render
// Disable equality check
const [obj, setObj] = createSignal({ x: 1 }, { equals: false });
setObj({ x: 1 }); // Trigger re-render
// Custom equality
const [data, setData] = createSignal(jsonString, {
equals: (a, b) => JSON.stringify(a) === JSON.stringify(b)
});Memo caching nilai computed. Recompute cuma kalau dependency berubah.
import { createSignal, createMemo } from 'solid-js';
function PriceCalculator() {
const [price, setPrice] = createSignal(100);
const [quantity, setQuantity] = createSignal(2);
const total = createMemo(() => price() * quantity());
return (
<div>
<p>Total: {total()}</p>
<button onClick={() => setQuantity(q => q + 1)}>Add</button>
</div>
);
}Memo sebagai read-only signal:
const first = createSignal(1);
const second = createSignal(2);
const sum = createMemo(() => first[0]() + second[0]());
// sum() = 3
// Recompute otomatis kalau first atau second berubahEffect jalan setiap kali dependency berubah. Untuk side effects (DOM, API calls, logging).
import { createSignal, createEffect } from 'solid-js';
function Logger() {
const [count, setCount] = createSignal(0);
// Jalan saat count berubah
createEffect(() => {
console.log('Count berubah:', count());
});
return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}import { createEffect, onCleanup } from 'solid-js';
function Timer() {
const [seconds, setSeconds] = createSignal(0);
createEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup saat component unmount atau effect re-run
onCleanup(() => clearInterval(interval));
});
return <p>Time: {seconds()}s</p>;
}Sama kayak createEffect tapi jalan saat render, bukan setelah. Tidak defer.
import { createRenderEffect } from 'solid-js';
createRenderEffect(() => {
document.title = `Page ${currentPage()}`;
});import { createEffect, on } from 'solid-js';
// Explicit dependencies
createEffect(on(count, () => {
console.log('Count changed to:', count());
}));
// Defer: jangan jalan saat pertama kali
createEffect(on(count, () => {
console.log('Count changed:', count());
}, { defer: true }));import { JSX } from 'solid-js';
interface GreetingProps {
name: string;
age?: number;
}
function Greeting(props: GreetingProps): JSX.Element {
return (
<div>
<h1>Hello, {props.name}!</h1>
{props.age && <p>Age: {props.age}</p>}
</div>
);
}
// Usage
<Greeting name="Galih" age={25} />// WRONG: destructuring props break reactivity di Solid
function Bad({ name }: { name: string }) {
return <h1>{name}</h1>;
}
// RIGHT: akses langsung dari props
function Good(props: { name: string }) {
return <h1>{props.name}</h1>;
}
// RIGHT: pakai splitProps atau mergeProps
import { splitProps } from 'solid-js';
function Card(props: { title: string; body: string; footer?: string }) {
const [local, others] = splitProps(props, ['title']);
return (
<div {...others}>
<h2>{local.title}</h2>
<p>{props.body}</p>
</div>
);
}import { splitProps, mergeProps } from 'solid-js';
// splitProps: pisahkan props ke beberapa object
function Button(allProps) {
const [buttonProps, rest] = splitProps(allProps, ['type', 'disabled']);
const [style, others] = splitProps(rest, ['class', 'style']);
return <button type={buttonProps.type} disabled={buttonProps.disabled}>Click</button>;
}
// mergeProps: gabungkan props (yang belakang menimpa)
function Greeting(props) {
const merged = mergeProps({ greeting: 'Hello', name: 'World' }, props);
return <h1>{merged.greeting}, {merged.name}!</h1>;
}Conditional rendering. Lebih efisien daripada ternary.
import { Show } from 'solid-js';
function App() {
const [loggedIn, setLoggedIn] = createSignal(false);
return (
<Show
when={loggedIn()}
fallback={<button onClick={login}>Login</button>}
>
<button onClick={logout}>Logout</button>
</Show>
);
}
// Nested Show
<Show when={data()}>
{(d) => <div>{d().name}</div>}
</Show>Looping array dengan key-based reconciliation.
import { For } from 'solid-js';
function TodoList() {
const [todos, setTodos] = createSignal([
{ id: 1, text: 'Belajar Solid' },
{ id: 2, text: 'Bikin project' },
]);
return (
<ul>
<For each={todos()}>
{(todo) => <li>{todo.text}</li>}
</For>
</ul>
);
}
// Dengan index
<For each={items()}>
{(item, index) => <div>{index()}: {item.name}</div>}
</For>Looping array dengan index-based reconciliation. Berguna kalau data ngga punya stable key.
import { Index } from 'solid-js';
<Index each={items()}>
{(item, index) => (
<div>{index}: {item()}</div>
)}
</Index>Multiple conditional branches.
import { Switch, Match } from 'solid-js';
function StatusMessage(props) {
return (
<Switch fallback={<p>Unknown status</p>}>
<Match when={props.status === 'loading'}>
<p>Loading...</p>
</Match>
<Match when={props.status === 'success'}>
<p>Success!</p>
</Match>
<Match when={props.status === 'error'}>
<p>Error occurred</p>
</Match>
</Switch>
);
}import { ErrorBoundary } from 'solid-js';
function App() {
return (
<ErrorBoundary
fallback={(err, reset) => (
<div>
<p>Error: {err.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}
>
<MyComponent />
</ErrorBoundary>
);
}import { Suspense } from 'solid-js';
import { lazy } from 'solid-js';
const LazyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}import { createContext, useContext } from 'solid-js';
// Bikin context
const ThemeContext = createContext<{ theme: string; toggle: () => void }>();
// Provider
function App() {
const [theme, setTheme] = createSignal('light');
const toggle = () => setTheme(t => t === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme: theme(), toggle }}>
<Header />
<Main />
</ThemeContext.Provider>
);
}
// Consumer
function Header() {
const ctx = useContext(ThemeContext);
return (
<header class={ctx?.theme}>
<button onClick={ctx?.toggle}>Toggle Theme</button>
</header>
);
}Fetch async data dengan tracking state (loading, error, data).
import { createResource } from 'solid-js';
// Fetcher function
async function fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
function UserProfile(props: { id: number }) {
const [user] = createResource(() => props.id, fetchUser);
return (
<div>
<Show when={!user.loading} fallback={<p>Loading...</p>}>
<h1>{user().name}</h1>
<p>{user().email}</p>
</Show>
</div>
);
}const [userId, setUserId] = createSignal(1);
const [user] = createResource(userId, fetchUser);
// userId berubah = refetch otomatis
<button onClick={() => setUserId(2)}>Next User</button>const [data, { refetch, mutate }] = createResource(fetchData);
// Refetch
refetch();
// Mutate: update data tanpa fetch
mutate({ ...data(), status: 'updated' });const [users] = createResource(fetchUsers);
const [posts] = createResource(() => users()?.[0]?.id, fetchPosts);
// posts di-fetch setelah users selesaiStore adalah reactive proxy untuk nested objects/arrays.
import { createStore } from 'solid-js/store';
function App() {
const [state, setState] = createStore({
user: { name: 'Galih', settings: { dark: false } },
todos: [{ id: 1, done: false, text: 'Belajar' }]
});
// Update nested
setState('user', 'name', 'Galih Pratama');
setState('user', 'settings', 'dark', true);
// Array update
setState('todos', 0, 'done', true);
setState('todos', t => [...t, { id: 2, done: false, text: 'Coding' }]);
return (
<div>
<p>{state.user.name}</p>
<p>Dark mode: {state.user.settings.dark ? 'On' : 'Off'}</p>
</div>
);
}import { createStore, produce } from 'solid-js/store';
const [state, setState] = createStore({ items: [{ id: 1, qty: 1 }] });
// Mutate secara draft
setState(produce(s => {
s.items[0].qty += 1;
s.items.push({ id: 2, qty: 3 });
}));Solid punya lifecycle yang minimal karena Signals + Effects udah cukup.
import { onMount, onCleanup } from 'solid-js';
function Component() {
// Setelah DOM pertama kali render
onMount(() => {
console.log('Mounted!');
const observer = new IntersectionObserver(callback);
observer.observe(document.getElementById('target'));
onCleanup(() => observer.disconnect());
});
}function Form() {
const [value, setValue] = createSignal('');
const handleSubmit = (e: Event) => {
e.preventDefault();
console.log('Submit:', value());
};
const handleInput = (e: InputEvent) => {
setValue((e.target as HTMLInputElement).value);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value()}
onInput={handleInput}
/>
<button type="submit">Submit</button>
</form>
);
}Solid delegasikan events ke document root secara default. Tapi bind:propertyName bisa dipake buat two-way binding.
// Controlled input
<input value={value()} onInput={e => setValue(e.currentTarget.value)} />
// bind: (Solid 1.8+)
<input bind:value={value} />import { createSignal } from 'solid-js';
function Canvas() {
let canvas: HTMLCanvasElement;
// canvas langsung tersedia setelah render
onMount(() => {
const ctx = canvas.getContext('2d');
ctx?.fillRect(0, 0, 100, 100);
});
return <canvas ref={canvas!} width={500} height={500} />;
}src/routes/
index.tsx -> /
about.tsx -> /about
users/
index.tsx -> /users
[id].tsx -> /users/:idimport { createRouteData } from 'solid-start';
export function routeData() {
return createRouteData(async () => {
const users = await fetch('https://api.example.com/users');
return users.json();
});
}
export default function Users() {
const users = useRouteData<typeof routeData>();
return (
<ul>
<For each={users()}>
{user => <li>{user.name}</li>}
</For>
</ul>
);
}| Aspek | Solid.js | React |
|---|---|---|
| Reactivity | Fine-grained Signals | Virtual DOM diffing |
| Re-render | Hanya komponen yang berubah | Komponen + children |
| State | createSignal (function) | useState (value) |
| Memo | Explicit createMemo | useMemo (deklarasi) |
| Effect | createEffect (auto-track) | useEffect (deps array) |
| Props | Reactive (akses via props.x) | Plain object |
| Virtual DOM | Tidak ada | Ada |
| Bundle size | ~7KB | ~45KB |
| SSR | SolidStart | Next.js |
Login atau daftar akun gratis untuk membaca cheat sheet ini.