SolidJS - True Reactivity Without Virtual DOM
Czym jest SolidJS?
SolidJS to nowoczesny framework JavaScript stworzony przez Ryana Carniato, który łączy znajomą składnię JSX (podobną do React) z prawdziwą reaktywnością na poziomie fine-grained. W przeciwieństwie do React, SolidJS nie używa Virtual DOM - komponenty kompilują się do optymalnych operacji DOM, co czyni go jednym z najszybszych frameworków w benchmarkach.
Framework wprowadza koncepcję Signals jako podstawową prymitywę reaktywności, która automatycznie śledzi zależności i aktualizuje tylko te części DOM, które faktycznie się zmieniły. To podejście eliminuje re-rendery całych komponentów i zapewnia wydajność porównywalną z vanilla JavaScript.
Dlaczego SolidJS?
Kluczowe zalety
- Najwyższa wydajność - Regularnie wygrywa w benchmarkach JS Framework Benchmark
- Fine-grained reactivity - Aktualizacje tylko tam, gdzie są potrzebne
- Zero Virtual DOM - Bezpośrednie operacje DOM
- Znajoma składnia JSX - Łatwe przejście z React
- Małe bundle - ~7KB gzipped dla core library
- Prosty model mentalny - Komponenty wykonują się raz
SolidJS vs React vs Vue vs Svelte
| Cecha | SolidJS | React | Vue 3 | Svelte |
|---|---|---|---|---|
| Virtual DOM | Nie | Tak | Tak | Nie |
| Reaktywność | Fine-grained | Component-level | Fine-grained | Compile-time |
| Bundle size | ~7KB | ~45KB | ~35KB | ~2KB |
| Performance | Najszybszy | Dobra | Bardzo dobra | Świetna |
| JSX | Natywny | Natywny | Opcjonalny | Własna składnia |
| Learning curve | Niska (dla React devs) | Średnia | Średnia | Niska |
| SSR | SolidStart | Next.js | Nuxt | SvelteKit |
| TypeScript | Świetny | Świetny | Świetny | Dobry |
Instalacja i konfiguracja
Tworzenie nowego projektu
# Z Vite template
npm create solid@latest my-app
# Interaktywny wizard zapyta o:
# - JavaScript/TypeScript
# - Solid/SolidStart (pełny framework)
# - Template (bare/todoMVC/hackernews)
cd my-app
npm install
npm run devStruktura projektu SolidStart
my-solid-app/
├── src/
│ ├── components/ # Komponenty
│ │ ├── Counter.tsx
│ │ └── Header.tsx
│ ├── routes/ # File-based routing
│ │ ├── index.tsx # /
│ │ ├── about.tsx # /about
│ │ └── blog/
│ │ ├── index.tsx # /blog
│ │ └── [slug].tsx # /blog/:slug
│ ├── lib/ # Utilities
│ ├── entry-client.tsx # Client entry
│ ├── entry-server.tsx # Server entry
│ └── root.tsx # Root layout
├── public/ # Static assets
├── vite.config.ts # Vite config
├── app.config.ts # SolidStart config
└── package.jsonPodstawowa konfiguracja
// vite.config.ts
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})// app.config.ts (SolidStart)
import { defineConfig } from '@solidjs/start/config'
export default defineConfig({
vite: {
// Vite options
},
server: {
preset: 'vercel', // lub 'node', 'cloudflare-pages', 'netlify'
},
})Podstawy SolidJS
Signals - reaktywny stan
import { createSignal } from 'solid-js'
function Counter() {
// createSignal zwraca [getter, setter]
const [count, setCount] = createSignal(0)
// WAŻNE: count() - signal to funkcja!
// W React: count, W SolidJS: count()
return (
<div>
<p>Count: {count()}</p>
<button onClick={() => setCount(count() + 1)}>
Increment
</button>
<button onClick={() => setCount(c => c - 1)}>
Decrement
</button>
<button onClick={() => setCount(0)}>
Reset
</button>
</div>
)
}Derived state (bez createMemo)
import { createSignal } from 'solid-js'
function Example() {
const [firstName, setFirstName] = createSignal('John')
const [lastName, setLastName] = createSignal('Doe')
// Derived state - prosta funkcja
// Automatycznie reaktywna dzięki fine-grained tracking
const fullName = () => `${firstName()} ${lastName()}`
return (
<div>
<input
value={firstName()}
onInput={(e) => setFirstName(e.target.value)}
/>
<input
value={lastName()}
onInput={(e) => setLastName(e.target.value)}
/>
<p>Full name: {fullName()}</p>
</div>
)
}createMemo - cached computed
import { createSignal, createMemo } from 'solid-js'
function ExpensiveComputation() {
const [items, setItems] = createSignal([1, 2, 3, 4, 5])
const [filter, setFilter] = createSignal('')
// createMemo cache'uje wynik - nie przelicza się przy każdym renderze
const expensiveSum = createMemo(() => {
console.log('Computing sum...')
return items().reduce((sum, item) => sum + item, 0)
})
const filteredItems = createMemo(() => {
const query = filter().toLowerCase()
return items().filter(item =>
String(item).toLowerCase().includes(query)
)
})
return (
<div>
<p>Sum: {expensiveSum()}</p>
<p>Filtered: {filteredItems().join(', ')}</p>
<input
placeholder="Filter..."
onInput={(e) => setFilter(e.target.value)}
/>
<button onClick={() => setItems([...items(), items().length + 1])}>
Add Item
</button>
</div>
)
}createEffect - side effects
import { createSignal, createEffect, onCleanup } from 'solid-js'
function EffectExample() {
const [searchQuery, setSearchQuery] = createSignal('')
const [results, setResults] = createSignal([])
// createEffect automatycznie trackuje dependencies
createEffect(() => {
const query = searchQuery()
if (query.length < 3) {
setResults([])
return
}
// Debounce
const timeoutId = setTimeout(async () => {
const response = await fetch(`/api/search?q=${query}`)
const data = await response.json()
setResults(data)
}, 300)
// Cleanup - wywoływane przy każdej zmianie lub unmount
onCleanup(() => clearTimeout(timeoutId))
})
// Effect z console log dla debugowania
createEffect(() => {
console.log('Search query changed:', searchQuery())
})
return (
<div>
<input
placeholder="Search..."
onInput={(e) => setSearchQuery(e.target.value)}
/>
<ul>
<For each={results()}>
{(result) => <li>{result}</li>}
</For>
</ul>
</div>
)
}onMount i onCleanup
import { createSignal, onMount, onCleanup } from 'solid-js'
function WindowSizeTracker() {
const [size, setSize] = createSignal({ width: 0, height: 0 })
// onMount - wykonuje się raz po zamontowaniu
onMount(() => {
// Dostęp do DOM i browser APIs
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
// onCleanup - cleanup przy unmount
onCleanup(() => {
window.removeEventListener('resize', handleResize)
})
})
return (
<div>
<p>Window: {size().width} x {size().height}</p>
</div>
)
}Control Flow Components
SolidJS używa komponentów do control flow zamiast JSX conditionals - to kluczowe dla fine-grained updates.
Show - warunkowe renderowanie
import { Show, createSignal } from 'solid-js'
function ConditionalExample() {
const [isLoggedIn, setIsLoggedIn] = createSignal(false)
const [user, setUser] = createSignal(null)
return (
<div>
{/* Podstawowe Show */}
<Show when={isLoggedIn()}>
<Dashboard />
</Show>
{/* Show z fallback */}
<Show when={isLoggedIn()} fallback={<LoginForm />}>
<Dashboard />
</Show>
{/* Show z keyed - user przekazany do children */}
<Show when={user()} keyed>
{(userData) => (
<div>
<h1>Welcome, {userData.name}</h1>
<p>Email: {userData.email}</p>
</div>
)}
</Show>
</div>
)
}For - iteracja po listach
import { For, createSignal } from 'solid-js'
interface Todo {
id: number
text: string
completed: boolean
}
function TodoList() {
const [todos, setTodos] = createSignal<Todo[]>([
{ id: 1, text: 'Learn Solid', completed: false },
{ id: 2, text: 'Build app', completed: false },
])
const toggleTodo = (id: number) => {
setTodos(todos =>
todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
)
}
const removeTodo = (id: number) => {
setTodos(todos => todos.filter(todo => todo.id !== id))
}
return (
<ul>
{/* For - wydajne iterowanie z automatycznym key */}
<For each={todos()}>
{(todo, index) => (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span class={todo.completed ? 'completed' : ''}>
{index() + 1}. {todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>×</button>
</li>
)}
</For>
</ul>
)
}Index - iteracja z stabilnym index
import { Index, createSignal } from 'solid-js'
function IndexExample() {
const [items, setItems] = createSignal(['A', 'B', 'C', 'D'])
// Index - index jest stały, wartość może się zmieniać
// For - wartość jest stała, index może się zmieniać
return (
<ul>
<Index each={items()}>
{(item, index) => (
<li>
{/* index to number, item() to signal */}
{index}: {item()}
<input
value={item()}
onInput={(e) => {
const newItems = [...items()]
newItems[index] = e.target.value
setItems(newItems)
}}
/>
</li>
)}
</Index>
</ul>
)
}Switch/Match - pattern matching
import { Switch, Match, createSignal } from 'solid-js'
function StatusDisplay() {
const [status, setStatus] = createSignal<'idle' | 'loading' | 'success' | 'error'>('idle')
const [data, setData] = createSignal(null)
const [error, setError] = createSignal(null)
return (
<div>
<Switch fallback={<p>Unknown status</p>}>
<Match when={status() === 'idle'}>
<p>Click to load data</p>
</Match>
<Match when={status() === 'loading'}>
<div class="spinner">Loading...</div>
</Match>
<Match when={status() === 'error'}>
<div class="error">
Error: {error()?.message}
<button onClick={() => setStatus('idle')}>Retry</button>
</div>
</Match>
<Match when={status() === 'success'}>
<div class="success">
<pre>{JSON.stringify(data(), null, 2)}</pre>
</div>
</Match>
</Switch>
</div>
)
}Dynamic - dynamiczny komponent
import { Dynamic, createSignal } from 'solid-js'
// Komponenty
const RedBox = () => <div style={{ background: 'red', padding: '20px' }}>Red</div>
const BlueBox = () => <div style={{ background: 'blue', padding: '20px' }}>Blue</div>
const GreenBox = () => <div style={{ background: 'green', padding: '20px' }}>Green</div>
function DynamicComponentExample() {
const [selected, setSelected] = createSignal<'red' | 'blue' | 'green'>('red')
const components = {
red: RedBox,
blue: BlueBox,
green: GreenBox,
}
return (
<div>
<select onChange={(e) => setSelected(e.target.value as any)}>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
{/* Dynamic renderuje wybrany komponent */}
<Dynamic component={components[selected()]} />
</div>
)
}Portal - renderowanie poza drzewem
import { Portal, createSignal } from 'solid-js'
function ModalExample() {
const [isOpen, setIsOpen] = createSignal(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Show when={isOpen()}>
{/* Portal renderuje children do document.body */}
<Portal>
<div class="modal-overlay" onClick={() => setIsOpen(false)}>
<div class="modal" onClick={(e) => e.stopPropagation()}>
<h2>Modal Title</h2>
<p>Modal content here</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
</div>
</Portal>
</Show>
</div>
)
}Stores - zarządzanie stanem
createStore - nested reactive state
import { createStore, produce } from 'solid-js/store'
interface User {
id: number
name: string
email: string
settings: {
theme: 'light' | 'dark'
notifications: boolean
}
}
interface AppState {
user: User | null
todos: { id: number; text: string; completed: boolean }[]
isLoading: boolean
}
function AppWithStore() {
// createStore dla złożonych obiektów
const [state, setState] = createStore<AppState>({
user: null,
todos: [],
isLoading: false,
})
// Aktualizacja nested property - path syntax
const setTheme = (theme: 'light' | 'dark') => {
setState('user', 'settings', 'theme', theme)
}
// Aktualizacja z produce (immer-like)
const addTodo = (text: string) => {
setState(produce((s) => {
s.todos.push({
id: Date.now(),
text,
completed: false,
})
}))
}
const toggleTodo = (id: number) => {
setState('todos', (todo) => todo.id === id, 'completed', (c) => !c)
}
// Login
const login = async (email: string, password: string) => {
setState('isLoading', true)
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
const user = await response.json()
setState('user', user)
} finally {
setState('isLoading', false)
}
}
return (
<div>
<Show when={state.user} fallback={<LoginForm onLogin={login} />}>
<p>Welcome, {state.user!.name}</p>
<button onClick={() => setTheme(
state.user!.settings.theme === 'light' ? 'dark' : 'light'
)}>
Toggle theme: {state.user!.settings.theme}
</button>
<For each={state.todos}>
{(todo) => (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</div>
)}
</For>
</Show>
</div>
)
}reconcile - efektywna aktualizacja
import { createStore, reconcile } from 'solid-js/store'
function DataGrid() {
const [data, setData] = createStore<{ items: Item[] }>({ items: [] })
const fetchData = async () => {
const response = await fetch('/api/items')
const newItems = await response.json()
// reconcile efektywnie porównuje i aktualizuje
// zamiast zastępować całą tablicę
setData('items', reconcile(newItems))
}
return (
<div>
<button onClick={fetchData}>Refresh</button>
<For each={data.items}>
{(item) => <ItemRow item={item} />}
</For>
</div>
)
}Context API
import { createContext, useContext, ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'
// Definicja typu context
interface AuthContextValue {
user: () => User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isLoading: () => boolean
}
// Tworzenie context
const AuthContext = createContext<AuthContextValue>()
// Provider
export const AuthProvider: ParentComponent = (props) => {
const [state, setState] = createStore({
user: null as User | null,
isLoading: false,
})
const login = async (email: string, password: string) => {
setState('isLoading', true)
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const user = await response.json()
setState('user', user)
} finally {
setState('isLoading', false)
}
}
const logout = () => {
setState('user', null)
}
const value: AuthContextValue = {
user: () => state.user,
login,
logout,
isLoading: () => state.isLoading,
}
return (
<AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>
)
}
// Hook do używania context
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
// Użycie
function Profile() {
const { user, logout } = useAuth()
return (
<Show when={user()}>
<div>
<p>Welcome, {user()!.name}</p>
<button onClick={logout}>Logout</button>
</div>
</Show>
)
}
// App
function App() {
return (
<AuthProvider>
<Header />
<Profile />
</AuthProvider>
)
}Resources - async data
import { createResource, Suspense, ErrorBoundary, Show } from 'solid-js'
// createResource dla async data
function UserProfile(props: { userId: string }) {
// Pierwszy argument: source signal
// Drugi argument: fetcher function
const [user, { mutate, refetch }] = createResource(
() => props.userId,
async (userId) => {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) throw new Error('User not found')
return response.json()
}
)
return (
<div>
<Show when={user.loading}>
<p>Loading...</p>
</Show>
<Show when={user.error}>
<p>Error: {user.error.message}</p>
<button onClick={refetch}>Retry</button>
</Show>
<Show when={user()}>
<div>
<h2>{user()!.name}</h2>
<p>{user()!.email}</p>
{/* Optimistic update */}
<button onClick={() => {
mutate({ ...user()!, name: 'Updated Name' })
// Następnie sync z serwerem
}}>
Update Name
</button>
</div>
</Show>
</div>
)
}
// Z Suspense
function App() {
const [userId, setUserId] = createSignal('1')
return (
<ErrorBoundary fallback={(err) => <p>Error: {err.message}</p>}>
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile userId={userId()} />
</Suspense>
</ErrorBoundary>
)
}SolidStart - Meta-Framework
Routing
// src/routes/index.tsx
export default function Home() {
return (
<main>
<h1>Welcome to SolidStart</h1>
</main>
)
}// src/routes/blog/[slug].tsx
import { useParams } from '@solidjs/router'
import { createAsync, cache } from '@solidjs/router'
// Cache dla data fetching
const getPost = cache(async (slug: string) => {
const response = await fetch(`/api/posts/${slug}`)
return response.json()
}, 'post')
export default function BlogPost() {
const params = useParams()
// createAsync - suspense-enabled data fetching
const post = createAsync(() => getPost(params.slug))
return (
<article>
<h1>{post()?.title}</h1>
<div innerHTML={post()?.content} />
</article>
)
}Server Functions
// src/routes/api/users.ts
import { json } from '@solidjs/router'
export async function GET() {
const users = await db.users.findMany()
return json(users)
}
export async function POST({ request }) {
const body = await request.json()
const user = await db.users.create({ data: body })
return json(user, { status: 201 })
}// Server actions w komponentach
import { action, useAction, useSubmission } from '@solidjs/router'
// Definicja server action
const addTodo = action(async (formData: FormData) => {
'use server'
const text = formData.get('text') as string
await db.todos.create({ data: { text } })
}, 'addTodo')
function TodoForm() {
const submission = useSubmission(addTodo)
return (
<form action={addTodo} method="post">
<input name="text" placeholder="New todo..." disabled={submission.pending} />
<button type="submit" disabled={submission.pending}>
{submission.pending ? 'Adding...' : 'Add'}
</button>
</form>
)
}Layouts
// src/routes/(app).tsx - Layout dla route group
import { ParentComponent } from 'solid-js'
const AppLayout: ParentComponent = (props) => {
return (
<div class="app-layout">
<Header />
<nav>
<A href="/dashboard">Dashboard</A>
<A href="/settings">Settings</A>
</nav>
<main>
{props.children}
</main>
<Footer />
</div>
)
}
export default AppLayoutKomponenty - wzorce
Props z defaults
import { Component, mergeProps, splitProps } from 'solid-js'
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
class?: string
children: JSX.Element
onClick?: () => void
}
const Button: Component<ButtonProps> = (props) => {
// mergeProps dla defaultów
const merged = mergeProps(
{ variant: 'primary', size: 'md', disabled: false },
props
)
// splitProps rozdziela props
const [local, others] = splitProps(merged, [
'variant', 'size', 'disabled', 'class', 'children'
])
const classes = () =>
`btn btn-${local.variant} btn-${local.size} ${local.class || ''}`
return (
<button
class={classes()}
disabled={local.disabled}
{...others}
>
{local.children}
</button>
)
}Ref forwarding
import { Component, onMount } from 'solid-js'
interface InputProps {
ref?: HTMLInputElement | ((el: HTMLInputElement) => void)
placeholder?: string
}
const Input: Component<InputProps> = (props) => {
let inputRef: HTMLInputElement
onMount(() => {
// Forward ref
if (typeof props.ref === 'function') {
props.ref(inputRef)
}
})
return (
<input
ref={inputRef!}
placeholder={props.placeholder}
class="input"
/>
)
}
// Użycie
function Form() {
let inputRef: HTMLInputElement
onMount(() => {
inputRef.focus()
})
return (
<form>
<Input ref={inputRef!} placeholder="Focus on mount" />
</form>
)
}Children handling
import { ParentComponent, children, JSX } from 'solid-js'
interface CardProps {
title?: string
children: JSX.Element
}
const Card: ParentComponent<CardProps> = (props) => {
// children() helper dla transformacji children
const resolved = children(() => props.children)
return (
<div class="card">
<Show when={props.title}>
<div class="card-header">{props.title}</div>
</Show>
<div class="card-body">
{resolved()}
</div>
</div>
)
}Integracje
Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: { extend: {} },
plugins: [],
}React Hook Form (lub własne)
import { createSignal, createEffect } from 'solid-js'
import { createStore } from 'solid-js/store'
// Prosty form hook dla Solid
function createForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = createStore(initialValues)
const [errors, setErrors] = createStore<Partial<Record<keyof T, string>>>({})
const [touched, setTouched] = createStore<Partial<Record<keyof T, boolean>>>({})
const setValue = (field: keyof T, value: any) => {
setValues(field as any, value)
}
const setError = (field: keyof T, error: string) => {
setErrors(field as any, error)
}
const setFieldTouched = (field: keyof T) => {
setTouched(field as any, true)
}
const reset = () => {
setValues(initialValues)
setErrors({})
setTouched({})
}
return {
values,
errors,
touched,
setValue,
setError,
setFieldTouched,
reset,
}
}
// Użycie
function ContactForm() {
const form = createForm({
name: '',
email: '',
message: '',
})
const handleSubmit = async (e: Event) => {
e.preventDefault()
// Walidacja i submit
}
return (
<form onSubmit={handleSubmit}>
<input
value={form.values.name}
onInput={(e) => form.setValue('name', e.target.value)}
onBlur={() => form.setFieldTouched('name')}
/>
{form.touched.name && form.errors.name && (
<span class="error">{form.errors.name}</span>
)}
{/* ... */}
</form>
)
}Deployment
Vercel
npm install @solidjs/start// app.config.ts
import { defineConfig } from '@solidjs/start/config'
export default defineConfig({
server: {
preset: 'vercel',
},
})Cloudflare Pages
// app.config.ts
export default defineConfig({
server: {
preset: 'cloudflare-pages',
},
})Static Export
// app.config.ts
export default defineConfig({
server: {
preset: 'static',
prerender: {
routes: ['/', '/about', '/blog'],
},
},
})Performance Tips
// 1. Unikaj niepotrzebnych wrapper'ów
// ❌
<div>{count()}</div>
// ✅
<span>{count()}</span>
// 2. Używaj createMemo dla kosztownych obliczeń
const expensive = createMemo(() => {
return items().filter(/* ... */).map(/* ... */)
})
// 3. Batch updates
import { batch } from 'solid-js'
batch(() => {
setCount(count() + 1)
setName('New name')
setItems([...items(), newItem])
})
// 4. untrack dla odczytu bez tworzenia dependency
import { untrack } from 'solid-js'
createEffect(() => {
const currentCount = count() // tracked
const staticValue = untrack(() => someSignal()) // not tracked
})
// 5. Lazy components
import { lazy } from 'solid-js'
const HeavyComponent = lazy(() => import('./HeavyComponent'))Cennik
- 100% darmowy - MIT License
FAQ - Często zadawane pytania
Czy mogę używać bibliotek React z SolidJS?
Nie bezpośrednio. SolidJS ma inny model reaktywności. Jednak wiele bibliotek ma odpowiedniki Solid lub możesz napisać wrapper. Społeczność aktywnie tworzy solid-* wersje popularnych bibliotek.
Dlaczego count() zamiast count?
Signals w SolidJS to funkcje. Wywołanie count() odczytuje wartość i rejestruje dependency. To pozwala na fine-grained tracking bez Virtual DOM.
Czy SolidJS nadaje się do dużych aplikacji?
Tak. SolidJS skaluje się doskonale dzięki fine-grained reactivity. Nie ma problemu z re-renderami dużych drzew komponentów jak w React.
Jaka jest krzywa uczenia się?
Dla developerów React jest bardzo niska. Składnia JSX jest prawie identyczna. Główna różnica to używanie count() zamiast count i komponenty control flow (Show, For).
Czy SolidJS ma ekosystem?
Rosnący. Jest SolidStart (meta-framework), solid-ui, solid-primitives (utility hooks), i wiele innych. Społeczność jest mniejsza niż React, ale bardzo aktywna.
SolidJS - True Reactivity Without Virtual DOM
What is SolidJS?
SolidJS is a modern JavaScript framework created by Ryan Carniato that combines familiar JSX syntax (similar to React) with true fine-grained reactivity. Unlike React, SolidJS does not use a Virtual DOM - components compile to optimal DOM operations, making it one of the fastest frameworks in benchmarks.
The framework introduces the concept of Signals as its core reactivity primitive, which automatically tracks dependencies and updates only the parts of the DOM that have actually changed. This approach eliminates re-renders of entire components and delivers performance comparable to vanilla JavaScript.
Why SolidJS?
Key advantages
- Best-in-class performance - Regularly wins in JS Framework Benchmark comparisons
- Fine-grained reactivity - Updates only where needed
- Zero Virtual DOM - Direct DOM operations
- Familiar JSX syntax - Easy transition from React
- Small bundle - ~7KB gzipped for the core library
- Simple mental model - Components execute once
SolidJS vs React vs Vue vs Svelte
| Feature | SolidJS | React | Vue 3 | Svelte |
|---|---|---|---|---|
| Virtual DOM | No | Yes | Yes | No |
| Reactivity | Fine-grained | Component-level | Fine-grained | Compile-time |
| Bundle size | ~7KB | ~45KB | ~35KB | ~2KB |
| Performance | Fastest | Good | Very good | Great |
| JSX | Native | Native | Optional | Custom syntax |
| Learning curve | Low (for React devs) | Medium | Medium | Low |
| SSR | SolidStart | Next.js | Nuxt | SvelteKit |
| TypeScript | Excellent | Excellent | Excellent | Good |
Installation and configuration
Creating a new project
# With Vite template
npm create solid@latest my-app
# The interactive wizard will ask about:
# - JavaScript/TypeScript
# - Solid/SolidStart (full framework)
# - Template (bare/todoMVC/hackernews)
cd my-app
npm install
npm run devSolidStart project structure
my-solid-app/
├── src/
│ ├── components/ # Components
│ │ ├── Counter.tsx
│ │ └── Header.tsx
│ ├── routes/ # File-based routing
│ │ ├── index.tsx # /
│ │ ├── about.tsx # /about
│ │ └── blog/
│ │ ├── index.tsx # /blog
│ │ └── [slug].tsx # /blog/:slug
│ ├── lib/ # Utilities
│ ├── entry-client.tsx # Client entry
│ ├── entry-server.tsx # Server entry
│ └── root.tsx # Root layout
├── public/ # Static assets
├── vite.config.ts # Vite config
├── app.config.ts # SolidStart config
└── package.jsonBasic configuration
// vite.config.ts
import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'
export default defineConfig({
plugins: [solid()],
})// app.config.ts (SolidStart)
import { defineConfig } from '@solidjs/start/config'
export default defineConfig({
vite: {
// Vite options
},
server: {
preset: 'vercel', // or 'node', 'cloudflare-pages', 'netlify'
},
})SolidJS basics
Signals - reactive state
import { createSignal } from 'solid-js'
function Counter() {
// createSignal returns [getter, setter]
const [count, setCount] = createSignal(0)
// IMPORTANT: count() - a signal is a function!
// In React: count, In SolidJS: count()
return (
<div>
<p>Count: {count()}</p>
<button onClick={() => setCount(count() + 1)}>
Increment
</button>
<button onClick={() => setCount(c => c - 1)}>
Decrement
</button>
<button onClick={() => setCount(0)}>
Reset
</button>
</div>
)
}Derived state (without createMemo)
import { createSignal } from 'solid-js'
function Example() {
const [firstName, setFirstName] = createSignal('John')
const [lastName, setLastName] = createSignal('Doe')
// Derived state - a simple function
// Automatically reactive thanks to fine-grained tracking
const fullName = () => `${firstName()} ${lastName()}`
return (
<div>
<input
value={firstName()}
onInput={(e) => setFirstName(e.target.value)}
/>
<input
value={lastName()}
onInput={(e) => setLastName(e.target.value)}
/>
<p>Full name: {fullName()}</p>
</div>
)
}createMemo - cached computed
import { createSignal, createMemo } from 'solid-js'
function ExpensiveComputation() {
const [items, setItems] = createSignal([1, 2, 3, 4, 5])
const [filter, setFilter] = createSignal('')
// createMemo caches the result - it doesn't recompute on every render
const expensiveSum = createMemo(() => {
console.log('Computing sum...')
return items().reduce((sum, item) => sum + item, 0)
})
const filteredItems = createMemo(() => {
const query = filter().toLowerCase()
return items().filter(item =>
String(item).toLowerCase().includes(query)
)
})
return (
<div>
<p>Sum: {expensiveSum()}</p>
<p>Filtered: {filteredItems().join(', ')}</p>
<input
placeholder="Filter..."
onInput={(e) => setFilter(e.target.value)}
/>
<button onClick={() => setItems([...items(), items().length + 1])}>
Add Item
</button>
</div>
)
}createEffect - side effects
import { createSignal, createEffect, onCleanup } from 'solid-js'
function EffectExample() {
const [searchQuery, setSearchQuery] = createSignal('')
const [results, setResults] = createSignal([])
// createEffect automatically tracks dependencies
createEffect(() => {
const query = searchQuery()
if (query.length < 3) {
setResults([])
return
}
// Debounce
const timeoutId = setTimeout(async () => {
const response = await fetch(`/api/search?q=${query}`)
const data = await response.json()
setResults(data)
}, 300)
// Cleanup - called on every change or unmount
onCleanup(() => clearTimeout(timeoutId))
})
// Effect with console log for debugging
createEffect(() => {
console.log('Search query changed:', searchQuery())
})
return (
<div>
<input
placeholder="Search..."
onInput={(e) => setSearchQuery(e.target.value)}
/>
<ul>
<For each={results()}>
{(result) => <li>{result}</li>}
</For>
</ul>
</div>
)
}onMount and onCleanup
import { createSignal, onMount, onCleanup } from 'solid-js'
function WindowSizeTracker() {
const [size, setSize] = createSignal({ width: 0, height: 0 })
// onMount - runs once after mounting
onMount(() => {
// Access to DOM and browser APIs
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
// onCleanup - cleanup on unmount
onCleanup(() => {
window.removeEventListener('resize', handleResize)
})
})
return (
<div>
<p>Window: {size().width} x {size().height}</p>
</div>
)
}Control flow components
SolidJS uses components for control flow instead of JSX conditionals - this is crucial for fine-grained updates.
Show - conditional rendering
import { Show, createSignal } from 'solid-js'
function ConditionalExample() {
const [isLoggedIn, setIsLoggedIn] = createSignal(false)
const [user, setUser] = createSignal(null)
return (
<div>
{/* Basic Show */}
<Show when={isLoggedIn()}>
<Dashboard />
</Show>
{/* Show with fallback */}
<Show when={isLoggedIn()} fallback={<LoginForm />}>
<Dashboard />
</Show>
{/* Show with keyed - user passed to children */}
<Show when={user()} keyed>
{(userData) => (
<div>
<h1>Welcome, {userData.name}</h1>
<p>Email: {userData.email}</p>
</div>
)}
</Show>
</div>
)
}For - iterating over lists
import { For, createSignal } from 'solid-js'
interface Todo {
id: number
text: string
completed: boolean
}
function TodoList() {
const [todos, setTodos] = createSignal<Todo[]>([
{ id: 1, text: 'Learn Solid', completed: false },
{ id: 2, text: 'Build app', completed: false },
])
const toggleTodo = (id: number) => {
setTodos(todos =>
todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
)
)
}
const removeTodo = (id: number) => {
setTodos(todos => todos.filter(todo => todo.id !== id))
}
return (
<ul>
{/* For - efficient iteration with automatic keying */}
<For each={todos()}>
{(todo, index) => (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span class={todo.completed ? 'completed' : ''}>
{index() + 1}. {todo.text}
</span>
<button onClick={() => removeTodo(todo.id)}>×</button>
</li>
)}
</For>
</ul>
)
}Index - iteration with stable index
import { Index, createSignal } from 'solid-js'
function IndexExample() {
const [items, setItems] = createSignal(['A', 'B', 'C', 'D'])
// Index - the index is stable, the value can change
// For - the value is stable, the index can change
return (
<ul>
<Index each={items()}>
{(item, index) => (
<li>
{/* index is a number, item() is a signal */}
{index}: {item()}
<input
value={item()}
onInput={(e) => {
const newItems = [...items()]
newItems[index] = e.target.value
setItems(newItems)
}}
/>
</li>
)}
</Index>
</ul>
)
}Switch/Match - pattern matching
import { Switch, Match, createSignal } from 'solid-js'
function StatusDisplay() {
const [status, setStatus] = createSignal<'idle' | 'loading' | 'success' | 'error'>('idle')
const [data, setData] = createSignal(null)
const [error, setError] = createSignal(null)
return (
<div>
<Switch fallback={<p>Unknown status</p>}>
<Match when={status() === 'idle'}>
<p>Click to load data</p>
</Match>
<Match when={status() === 'loading'}>
<div class="spinner">Loading...</div>
</Match>
<Match when={status() === 'error'}>
<div class="error">
Error: {error()?.message}
<button onClick={() => setStatus('idle')}>Retry</button>
</div>
</Match>
<Match when={status() === 'success'}>
<div class="success">
<pre>{JSON.stringify(data(), null, 2)}</pre>
</div>
</Match>
</Switch>
</div>
)
}Dynamic - dynamic component
import { Dynamic, createSignal } from 'solid-js'
// Components
const RedBox = () => <div style={{ background: 'red', padding: '20px' }}>Red</div>
const BlueBox = () => <div style={{ background: 'blue', padding: '20px' }}>Blue</div>
const GreenBox = () => <div style={{ background: 'green', padding: '20px' }}>Green</div>
function DynamicComponentExample() {
const [selected, setSelected] = createSignal<'red' | 'blue' | 'green'>('red')
const components = {
red: RedBox,
blue: BlueBox,
green: GreenBox,
}
return (
<div>
<select onChange={(e) => setSelected(e.target.value as any)}>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
{/* Dynamic renders the selected component */}
<Dynamic component={components[selected()]} />
</div>
)
}Portal - rendering outside the tree
import { Portal, createSignal } from 'solid-js'
function ModalExample() {
const [isOpen, setIsOpen] = createSignal(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<Show when={isOpen()}>
{/* Portal renders children into document.body */}
<Portal>
<div class="modal-overlay" onClick={() => setIsOpen(false)}>
<div class="modal" onClick={(e) => e.stopPropagation()}>
<h2>Modal Title</h2>
<p>Modal content here</p>
<button onClick={() => setIsOpen(false)}>Close</button>
</div>
</div>
</Portal>
</Show>
</div>
)
}Stores - state management
createStore - nested reactive state
import { createStore, produce } from 'solid-js/store'
interface User {
id: number
name: string
email: string
settings: {
theme: 'light' | 'dark'
notifications: boolean
}
}
interface AppState {
user: User | null
todos: { id: number; text: string; completed: boolean }[]
isLoading: boolean
}
function AppWithStore() {
// createStore for complex objects
const [state, setState] = createStore<AppState>({
user: null,
todos: [],
isLoading: false,
})
// Updating a nested property - path syntax
const setTheme = (theme: 'light' | 'dark') => {
setState('user', 'settings', 'theme', theme)
}
// Updating with produce (immer-like)
const addTodo = (text: string) => {
setState(produce((s) => {
s.todos.push({
id: Date.now(),
text,
completed: false,
})
}))
}
const toggleTodo = (id: number) => {
setState('todos', (todo) => todo.id === id, 'completed', (c) => !c)
}
// Login
const login = async (email: string, password: string) => {
setState('isLoading', true)
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
const user = await response.json()
setState('user', user)
} finally {
setState('isLoading', false)
}
}
return (
<div>
<Show when={state.user} fallback={<LoginForm onLogin={login} />}>
<p>Welcome, {state.user!.name}</p>
<button onClick={() => setTheme(
state.user!.settings.theme === 'light' ? 'dark' : 'light'
)}>
Toggle theme: {state.user!.settings.theme}
</button>
<For each={state.todos}>
{(todo) => (
<div>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
</div>
)}
</For>
</Show>
</div>
)
}reconcile - efficient updates
import { createStore, reconcile } from 'solid-js/store'
function DataGrid() {
const [data, setData] = createStore<{ items: Item[] }>({ items: [] })
const fetchData = async () => {
const response = await fetch('/api/items')
const newItems = await response.json()
// reconcile efficiently diffs and updates
// instead of replacing the entire array
setData('items', reconcile(newItems))
}
return (
<div>
<button onClick={fetchData}>Refresh</button>
<For each={data.items}>
{(item) => <ItemRow item={item} />}
</For>
</div>
)
}Context API
import { createContext, useContext, ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'
// Context type definition
interface AuthContextValue {
user: () => User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isLoading: () => boolean
}
// Creating context
const AuthContext = createContext<AuthContextValue>()
// Provider
export const AuthProvider: ParentComponent = (props) => {
const [state, setState] = createStore({
user: null as User | null,
isLoading: false,
})
const login = async (email: string, password: string) => {
setState('isLoading', true)
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const user = await response.json()
setState('user', user)
} finally {
setState('isLoading', false)
}
}
const logout = () => {
setState('user', null)
}
const value: AuthContextValue = {
user: () => state.user,
login,
logout,
isLoading: () => state.isLoading,
}
return (
<AuthContext.Provider value={value}>
{props.children}
</AuthContext.Provider>
)
}
// Hook for using context
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
// Usage
function Profile() {
const { user, logout } = useAuth()
return (
<Show when={user()}>
<div>
<p>Welcome, {user()!.name}</p>
<button onClick={logout}>Logout</button>
</div>
</Show>
)
}
// App
function App() {
return (
<AuthProvider>
<Header />
<Profile />
</AuthProvider>
)
}Resources - async data
import { createResource, Suspense, ErrorBoundary, Show } from 'solid-js'
// createResource for async data
function UserProfile(props: { userId: string }) {
// First argument: source signal
// Second argument: fetcher function
const [user, { mutate, refetch }] = createResource(
() => props.userId,
async (userId) => {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) throw new Error('User not found')
return response.json()
}
)
return (
<div>
<Show when={user.loading}>
<p>Loading...</p>
</Show>
<Show when={user.error}>
<p>Error: {user.error.message}</p>
<button onClick={refetch}>Retry</button>
</Show>
<Show when={user()}>
<div>
<h2>{user()!.name}</h2>
<p>{user()!.email}</p>
{/* Optimistic update */}
<button onClick={() => {
mutate({ ...user()!, name: 'Updated Name' })
// Then sync with the server
}}>
Update Name
</button>
</div>
</Show>
</div>
)
}
// With Suspense
function App() {
const [userId, setUserId] = createSignal('1')
return (
<ErrorBoundary fallback={(err) => <p>Error: {err.message}</p>}>
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile userId={userId()} />
</Suspense>
</ErrorBoundary>
)
}SolidStart - meta-framework
Routing
// src/routes/index.tsx
export default function Home() {
return (
<main>
<h1>Welcome to SolidStart</h1>
</main>
)
}// src/routes/blog/[slug].tsx
import { useParams } from '@solidjs/router'
import { createAsync, cache } from '@solidjs/router'
// Cache for data fetching
const getPost = cache(async (slug: string) => {
const response = await fetch(`/api/posts/${slug}`)
return response.json()
}, 'post')
export default function BlogPost() {
const params = useParams()
// createAsync - suspense-enabled data fetching
const post = createAsync(() => getPost(params.slug))
return (
<article>
<h1>{post()?.title}</h1>
<div innerHTML={post()?.content} />
</article>
)
}Server functions
// src/routes/api/users.ts
import { json } from '@solidjs/router'
export async function GET() {
const users = await db.users.findMany()
return json(users)
}
export async function POST({ request }) {
const body = await request.json()
const user = await db.users.create({ data: body })
return json(user, { status: 201 })
}// Server actions in components
import { action, useAction, useSubmission } from '@solidjs/router'
// Server action definition
const addTodo = action(async (formData: FormData) => {
'use server'
const text = formData.get('text') as string
await db.todos.create({ data: { text } })
}, 'addTodo')
function TodoForm() {
const submission = useSubmission(addTodo)
return (
<form action={addTodo} method="post">
<input name="text" placeholder="New todo..." disabled={submission.pending} />
<button type="submit" disabled={submission.pending}>
{submission.pending ? 'Adding...' : 'Add'}
</button>
</form>
)
}Layouts
// src/routes/(app).tsx - Layout for route group
import { ParentComponent } from 'solid-js'
const AppLayout: ParentComponent = (props) => {
return (
<div class="app-layout">
<Header />
<nav>
<A href="/dashboard">Dashboard</A>
<A href="/settings">Settings</A>
</nav>
<main>
{props.children}
</main>
<Footer />
</div>
)
}
export default AppLayoutComponents - patterns
Props with defaults
import { Component, mergeProps, splitProps } from 'solid-js'
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
class?: string
children: JSX.Element
onClick?: () => void
}
const Button: Component<ButtonProps> = (props) => {
// mergeProps for defaults
const merged = mergeProps(
{ variant: 'primary', size: 'md', disabled: false },
props
)
// splitProps separates props
const [local, others] = splitProps(merged, [
'variant', 'size', 'disabled', 'class', 'children'
])
const classes = () =>
`btn btn-${local.variant} btn-${local.size} ${local.class || ''}`
return (
<button
class={classes()}
disabled={local.disabled}
{...others}
>
{local.children}
</button>
)
}Ref forwarding
import { Component, onMount } from 'solid-js'
interface InputProps {
ref?: HTMLInputElement | ((el: HTMLInputElement) => void)
placeholder?: string
}
const Input: Component<InputProps> = (props) => {
let inputRef: HTMLInputElement
onMount(() => {
// Forward ref
if (typeof props.ref === 'function') {
props.ref(inputRef)
}
})
return (
<input
ref={inputRef!}
placeholder={props.placeholder}
class="input"
/>
)
}
// Usage
function Form() {
let inputRef: HTMLInputElement
onMount(() => {
inputRef.focus()
})
return (
<form>
<Input ref={inputRef!} placeholder="Focus on mount" />
</form>
)
}Children handling
import { ParentComponent, children, JSX } from 'solid-js'
interface CardProps {
title?: string
children: JSX.Element
}
const Card: ParentComponent<CardProps> = (props) => {
// children() helper for transforming children
const resolved = children(() => props.children)
return (
<div class="card">
<Show when={props.title}>
<div class="card-header">{props.title}</div>
</Show>
<div class="card-body">
{resolved()}
</div>
</div>
)
}Integrations
Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: { extend: {} },
plugins: [],
}Form handling (custom solution)
import { createSignal, createEffect } from 'solid-js'
import { createStore } from 'solid-js/store'
// Simple form hook for Solid
function createForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = createStore(initialValues)
const [errors, setErrors] = createStore<Partial<Record<keyof T, string>>>({})
const [touched, setTouched] = createStore<Partial<Record<keyof T, boolean>>>({})
const setValue = (field: keyof T, value: any) => {
setValues(field as any, value)
}
const setError = (field: keyof T, error: string) => {
setErrors(field as any, error)
}
const setFieldTouched = (field: keyof T) => {
setTouched(field as any, true)
}
const reset = () => {
setValues(initialValues)
setErrors({})
setTouched({})
}
return {
values,
errors,
touched,
setValue,
setError,
setFieldTouched,
reset,
}
}
// Usage
function ContactForm() {
const form = createForm({
name: '',
email: '',
message: '',
})
const handleSubmit = async (e: Event) => {
e.preventDefault()
// Validation and submit
}
return (
<form onSubmit={handleSubmit}>
<input
value={form.values.name}
onInput={(e) => form.setValue('name', e.target.value)}
onBlur={() => form.setFieldTouched('name')}
/>
{form.touched.name && form.errors.name && (
<span class="error">{form.errors.name}</span>
)}
{/* ... */}
</form>
)
}Deployment
Vercel
npm install @solidjs/start// app.config.ts
import { defineConfig } from '@solidjs/start/config'
export default defineConfig({
server: {
preset: 'vercel',
},
})Cloudflare Pages
// app.config.ts
export default defineConfig({
server: {
preset: 'cloudflare-pages',
},
})Static export
// app.config.ts
export default defineConfig({
server: {
preset: 'static',
prerender: {
routes: ['/', '/about', '/blog'],
},
},
})Performance tips
// 1. Avoid unnecessary wrappers
// ❌
<div>{count()}</div>
// ✅
<span>{count()}</span>
// 2. Use createMemo for expensive computations
const expensive = createMemo(() => {
return items().filter(/* ... */).map(/* ... */)
})
// 3. Batch updates
import { batch } from 'solid-js'
batch(() => {
setCount(count() + 1)
setName('New name')
setItems([...items(), newItem])
})
// 4. untrack for reading without creating a dependency
import { untrack } from 'solid-js'
createEffect(() => {
const currentCount = count() // tracked
const staticValue = untrack(() => someSignal()) // not tracked
})
// 5. Lazy components
import { lazy } from 'solid-js'
const HeavyComponent = lazy(() => import('./HeavyComponent'))Pricing
- 100% free - MIT License
FAQ - frequently asked questions
Can I use React libraries with SolidJS?
Not directly. SolidJS has a different reactivity model. However, many libraries have Solid equivalents or you can write a wrapper. The community is actively creating solid-* versions of popular libraries.
Why count() instead of count?
Signals in SolidJS are functions. Calling count() reads the value and registers a dependency. This enables fine-grained tracking without a Virtual DOM.
Is SolidJS suitable for large applications?
Yes. SolidJS scales excellently thanks to fine-grained reactivity. It does not suffer from the problem of re-rendering large component trees like React does.
What is the learning curve like?
For React developers it is very low. The JSX syntax is nearly identical. The main differences are using count() instead of count and control flow components (Show, For).
Does SolidJS have an ecosystem?
A growing one. There is SolidStart (meta-framework), solid-ui, solid-primitives (utility hooks), and many others. The community is smaller than React's, but very active.