Pular para o conteúdo

SolidJS

Framework JavaScript de UI reativo com granularidade fina, sem DOM virtual e com desempenho excepcional.

Instalação

Criar Novo Projeto

ComandoDescrição
npx degit solidjs/templates/ts my-appCriar projeto SolidJS com TypeScript
npx degit solidjs/templates/js my-appCriar projeto SolidJS com JavaScript
npm create solid@latestCriar projeto com a CLI do SolidStart
npm install solid-jsInstalar SolidJS em projeto existente
npm install solid-startInstalar o meta-framework SolidStart

Comandos de Desenvolvimento

ComandoDescrição
npm run devIniciar servidor de desenvolvimento com hot reload
npm run buildCompilar para produção
npm run serveVisualizar build de produção localmente
npm run startIniciar servidor SolidStart

Templates de Projetos

# Bare SolidJS (Vite)
npx degit solidjs/templates/ts my-app
cd my-app && npm install

# SolidStart (full-stack meta-framework)
npm create solid@latest my-app
# Choose template: bare, with-auth, with-mdx, with-prisma, with-tailwindcss

# Community templates
npx degit solidjs/templates/ts-windicss my-app    # WindiCSS
npx degit solidjs/templates/ts-unocss my-app      # UnoCSS
npx degit solidjs/templates/ts-bootstrap my-app    # Bootstrap
npx degit solidjs/templates/ts-sass my-app         # Sass

Componentes e JSX

Componentes Básicos

ComandoDescrição
function App() { return <div>Hello</div> }Definir um componente básico
<MyComponent name="value" />Passar props para um componente
props.childrenAcessar conteúdo filho
<div class={styles.container}>Aplicar classe CSS (use class, não className)
<div classList={{ active: isActive() }}>Classes CSS condicionais
<div style={{ color: 'red', 'font-size': '14px' }}>Aplicar estilos inline
<div innerHTML={htmlString} />Definir conteúdo HTML bruto
<div ref={myRef}>Anexar referência ao DOM

Manipulação de Eventos

ComandoDescrição
<div onClick={handler}>Anexar evento delegado
<div on:click={handler}>Anexar evento DOM nativo (ignora delegação)
<div onInput={(e) => setValue(e.target.value)}>Lidar com eventos de input
<div on:keydown={(e) => handleKey(e)}>Lidar com eventos de teclado nativamente
<div onClick={[handler, data]}>Passar dados com manipulador de evento

Padrões de Componentes

import { type Component, type ParentComponent } from 'solid-js'

// Typed component with props
interface UserProps {
  name: string
  age: number
  role?: string
}

const UserCard: Component<UserProps> = (props) => {
  return (
    <div class="card">
      <h2>{props.name}</h2>
      <p>Age: {props.age}</p>
      <p>Role: {props.role ?? 'Member'}</p>
    </div>
  )
}

// Parent component (accepts children)
const Layout: ParentComponent = (props) => {
  return (
    <div class="layout">
      <header>My App</header>
      <main>{props.children}</main>
      <footer>Footer</footer>
    </div>
  )
}

// Splitting props for forwarding
import { splitProps } from 'solid-js'

const Button: Component<ButtonProps> = (props) => {
  const [local, rest] = splitProps(props, ['variant', 'size'])
  return (
    <button
      class={`btn btn-${local.variant} btn-${local.size}`}
      {...rest}
    />
  )
}

// Merging default props
import { mergeProps } from 'solid-js'

const Alert: Component<AlertProps> = (rawProps) => {
  const props = mergeProps({ type: 'info', dismissible: false }, rawProps)
  return <div class={`alert alert-${props.type}`}>{props.children}</div>
}

Primitivas Reativas

Signals

ComandoDescrição
const [count, setCount] = createSignal(0)Criar um signal reativo
count()Ler valor do signal (deve chamar como função)
setCount(5)Definir signal para valor específico
setCount(prev => prev + 1)Atualizar signal com valor anterior
const [name, setName] = createSignal<string>()Criar signal tipado

Effects e Memos

ComandoDescrição
createEffect(() => console.log(count()))Executar efeito colateral quando signal mudar
createEffect(on(count, (v) => log(v)))Rastrear signal específico explicitamente
createEffect(on([a, b], ([a, b]) => ...))Rastrear múltiplos signals explicitamente
const double = createMemo(() => count() * 2)Criar valor derivado/computado
createRenderEffect(() => ...)Efeito que executa antes da pintura do DOM
createComputed(() => ...)Efeito síncrono durante a renderização

Ciclo de Vida

ComandoDescrição
onMount(() => { ... })Executar uma vez quando o componente montar
onCleanup(() => { ... })Executar limpeza quando o componente desmontar
batch(() => { setA(1); setB(2) })Agrupar múltiplas atualizações de signals
untrack(() => value())Ler signal sem rastrear dependência

Padrões Reativos

import { createSignal, createEffect, createMemo, on, batch, untrack } from 'solid-js'

function Counter() {
  const [count, setCount] = createSignal(0)
  const [step, setStep] = createSignal(1)

  // Derived value (memoized, recalculates only when count changes)
  const doubled = createMemo(() => count() * 2)
  const isEven = createMemo(() => count() % 2 === 0)

  // Effect: runs when any accessed signal changes
  createEffect(() => {
    console.log(`Count is now: ${count()}`)
    // Automatically tracks count() as a dependency
  })

  // Explicit tracking with on()
  createEffect(on(count, (value, prev) => {
    console.log(`Changed from ${prev} to ${value}`)
  }, { defer: true })) // defer: skip initial run

  // Batch updates (single re-render)
  const reset = () => batch(() => {
    setCount(0)
    setStep(1)
  })

  // Read without creating dependency
  const logWithoutTracking = () => {
    const current = untrack(() => count())
    console.log('Snapshot:', current)
  }

  return (
    <div>
      <p>Count: {count()} (doubled: {doubled()}, even: {String(isEven())})</p>
      <button onClick={() => setCount(c => c + step())}>
        Add {step()}
      </button>
      <button onClick={reset}>Reset</button>
    </div>
  )
}

Fluxo de Controle

Componentes Integrados

ComandoDescrição
<Show when={loggedIn()} fallback={<Login/>}>Renderização condicional
<For each={items()}>{(item) => <li>{item}</li>}</For>Renderizar lista (chaveado por referência)
<Index each={items()}>{(item, i) => <li>{item()}</li>}</Index>Renderizar lista (chaveado por índice)
<Switch><Match when={a()}>A</Match></Switch>Renderização switch/case
<ErrorBoundary fallback={err => <p>{err}</p>}>Capturar erros de renderização
<Suspense fallback={<Loading/>}>Mostrar fallback durante carregamento assíncrono
<Dynamic component={MyComp} />Renderizar componente dinâmico
<Portal mount={document.body}>Renderizar em nó DOM diferente

Exemplos de Fluxo de Controle

import { Show, For, Index, Switch, Match, Suspense, ErrorBoundary } from 'solid-js'

function Dashboard() {
  const [user, setUser] = createSignal(null)
  const [items, setItems] = createSignal([
    { id: 1, name: 'Alpha', status: 'active' },
    { id: 2, name: 'Beta', status: 'inactive' },
  ])
  const [view, setView] = createSignal('list')

  return (
    <div>
      {/* Show: conditional rendering with fallback */}
      <Show when={user()} fallback={<p>Please log in</p>}>
        {(u) => <p>Welcome, {u().name}!</p>}
      </Show>

      {/* For: keyed by reference (best for objects) */}
      <For each={items()}>
        {(item, index) => (
          <div>
            <span>{index() + 1}. {item.name}</span>
            <span> ({item.status})</span>
          </div>
        )}
      </For>

      {/* Switch/Match: multiple conditions */}
      <Switch fallback={<p>Unknown view</p>}>
        <Match when={view() === 'list'}>
          <ListView />
        </Match>
        <Match when={view() === 'grid'}>
          <GridView />
        </Match>
        <Match when={view() === 'table'}>
          <TableView />
        </Match>
      </Switch>

      {/* ErrorBoundary: catch errors in child tree */}
      <ErrorBoundary fallback={(err, reset) => (
        <div>
          <p>Error: {err.message}</p>
          <button onClick={reset}>Try Again</button>
        </div>
      )}>
        <RiskyComponent />
      </ErrorBoundary>
    </div>
  )
}

Stores e Estado

Operações de Store

ComandoDescrição
const [store, setStore] = createStore({})Criar uma store reativa
setStore('name', 'Alice')Atualizar propriedade da store por caminho
setStore('users', 0, 'name', 'Bob')Atualizar propriedade aninhada da store
setStore('list', l => [...l, item])Adicionar item ao array da store
setStore('list', i => i.id === 1, 'done', true)Atualizar itens correspondentes no array
produce(s => { s.count++ })Atualizações de store no estilo mutável
reconcile(newData)Substituir dados da store eficientemente
unwrap(store)Obter dados brutos não-proxied

Padrões de Store

import { createStore, produce, reconcile, unwrap } from 'solid-js/store'

interface Todo {
  id: number
  text: string
  completed: boolean
}

interface AppState {
  todos: Todo[]
  filter: 'all' | 'active' | 'completed'
  user: { name: string; settings: { theme: string } }
}

function TodoApp() {
  const [state, setState] = createStore<AppState>({
    todos: [],
    filter: 'all',
    user: { name: 'Alice', settings: { theme: 'dark' } },
  })

  // Add item
  const addTodo = (text: string) => {
    setState('todos', todos => [
      ...todos,
      { id: Date.now(), text, completed: false }
    ])
  }

  // Toggle specific item by matching
  const toggleTodo = (id: number) => {
    setState('todos', todo => todo.id === id, 'completed', c => !c)
  }

  // Delete item (filter produces new array)
  const deleteTodo = (id: number) => {
    setState('todos', todos => todos.filter(t => t.id !== id))
  }

  // Mutable-style updates with produce
  const clearCompleted = () => {
    setState(produce((s) => {
      s.todos = s.todos.filter(t => !t.completed)
    }))
  }

  // Deep nested update
  const setTheme = (theme: string) => {
    setState('user', 'settings', 'theme', theme)
  }

  // Replace entire store data (reconcile diffs efficiently)
  const loadFromServer = async () => {
    const data = await fetch('/api/todos').then(r => r.json())
    setState('todos', reconcile(data))
  }

  return (/* ... */)
}

Resource e Busca de Dados

Operações de Resource

ComandoDescrição
const [data] = createResource(fetchFn)Criar resource assíncrono
const [data] = createResource(id, fetchFn)Resource com signal de origem reativo
data()Acessar dados do resource
data.loadingVerificar se o resource está carregando
data.errorAcessar erro do resource
data.latestObter último valor resolvido (sobrevive ao refetch)
data.stateObter estado do resource (‘unresolved’, ‘pending’, ‘ready’, ‘errored’)
const { refetch } = dataDisparar refetch manualmente
createResource(source, fetcher, { initialValue })Resource com valor inicial

Padrões de Busca de Dados

import { createResource, createSignal, Suspense, ErrorBoundary } from 'solid-js'

interface User {
  id: number
  name: string
  email: string
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error(`User ${id} not found`)
  return res.json()
}

function UserProfile() {
  const [userId, setUserId] = createSignal(1)

  // Resource refetches automatically when userId() changes
  const [user, { refetch, mutate }] = createResource(userId, fetchUser)

  // Optimistic update
  const updateName = async (newName: string) => {
    const prev = user()
    mutate({ ...prev!, name: newName }) // optimistic
    try {
      await fetch(`/api/users/${userId()}`, {
        method: 'PATCH',
        body: JSON.stringify({ name: newName }),
      })
    } catch {
      mutate(prev) // rollback on error
    }
  }

  return (
    <ErrorBoundary fallback={<p>Failed to load user</p>}>
      <Suspense fallback={<p>Loading user...</p>}>
        <div>
          <h2>{user()?.name}</h2>
          <p>{user()?.email}</p>
          <p>State: {user.state}</p>
          <button onClick={refetch} disabled={user.loading}>
            Refresh
          </button>
          <button onClick={() => setUserId(id => id + 1)}>
            Next User
          </button>
        </div>
      </Suspense>
    </ErrorBoundary>
  )
}

Roteamento

@solidjs/router

ComandoDescrição
npm install @solidjs/routerInstalar roteador SolidJS
<Router><Route path="/" component={Home}/></Router>Definir rota básica
<Route path="/users/:id" component={User}/>Rota com parâmetro
<Route path="/*all" component={NotFound}/>Rota catch-all
const params = useParams()Acessar parâmetros da rota
const [searchParams, setSearchParams] = useSearchParams()Acessar parâmetros de query
const navigate = useNavigate()Navegação programática
navigate('/dashboard')Navegar para caminho
<A href="/about">About</A>Componente de link de navegação
<A href="/about" activeClass="active">Link com estilo ativo

Configuração do Roteador

import { Router, Route, A, useParams, useNavigate, useSearchParams } from '@solidjs/router'
import { lazy } from 'solid-js'

// Lazy-loaded routes
const Dashboard = lazy(() => import('./pages/Dashboard'))
const UserProfile = lazy(() => import('./pages/UserProfile'))
const Settings = lazy(() => import('./pages/Settings'))

function App() {
  return (
    <Router>
      <nav>
        <A href="/" activeClass="active" end>Home</A>
        <A href="/dashboard" activeClass="active">Dashboard</A>
        <A href="/settings" activeClass="active">Settings</A>
      </nav>
      <Route path="/" component={Home} />
      <Route path="/dashboard" component={Dashboard} />
      <Route path="/users/:id" component={UserProfile} />
      <Route path="/settings" component={Settings} />
      <Route path="/*all" component={NotFound} />
    </Router>
  )
}

// Nested routes
function App() {
  return (
    <Router>
      <Route path="/admin" component={AdminLayout}>
        <Route path="/" component={AdminDashboard} />
        <Route path="/users" component={AdminUsers} />
        <Route path="/users/:id" component={AdminUserDetail} />
      </Route>
    </Router>
  )
}

Contexto e Injeção de Dependência

API de Contexto

import { createContext, useContext, type ParentComponent } from 'solid-js'
import { createStore } from 'solid-js/store'

interface AuthState {
  user: { name: string; role: string } | null
  token: string | null
}

interface AuthContextValue {
  state: AuthState
  login: (token: string, user: AuthState['user']) => void
  logout: () => void
  isAuthenticated: () => boolean
}

const AuthContext = createContext<AuthContextValue>()

const AuthProvider: ParentComponent = (props) => {
  const [state, setState] = createStore<AuthState>({
    user: null,
    token: null,
  })

  const value: AuthContextValue = {
    state,
    login: (token, user) => {
      setState({ token, user })
    },
    logout: () => {
      setState({ token: null, user: null })
    },
    isAuthenticated: () => state.token !== null,
  }

  return (
    <AuthContext.Provider value={value}>
      {props.children}
    </AuthContext.Provider>
  )
}

function useAuth() {
  const context = useContext(AuthContext)
  if (!context) throw new Error('useAuth must be used within AuthProvider')
  return context
}

// Usage
function UserMenu() {
  const auth = useAuth()
  return (
    <Show when={auth.isAuthenticated()} fallback={<LoginButton />}>
      <p>Hello, {auth.state.user?.name}</p>
      <button onClick={auth.logout}>Logout</button>
    </Show>
  )
}

Estilização

Opções de CSS

ComandoDescrição
import styles from './App.module.css'Importar módulos CSS
<div class={styles.container}>Aplicar classe de módulo CSS
npm install solid-styled-componentsInstalar styled-components para Solid
const Btn = styled('button')\color: red“Criar componente estilizado
<div class="static-class">Aplicar classe CSS estática

Integração com Tailwind CSS

# Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
export default {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: { extend: {} },
  plugins: [],
}
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

SolidStart (Meta-Framework)

Funcionalidades do SolidStart

ComandoDescrição
npm create solid@latestCriar projeto SolidStart
"use server" directiveMarcar função como exclusiva do servidor
createServerAction$()Criar uma ação de servidor
createRouteData()Carregar dados para uma rota no servidor
<Title>Page Title</Title>Definir título da página (de @solidjs/meta)
<Meta name="description" content="..." />Definir meta tags

Funções e Ações do Servidor

// src/routes/todos.tsx
import { createAsync, query, action, redirect } from '@solidjs/router'

// Server query (data loading)
const getTodos = query(async () => {
  'use server'
  const db = await getDatabase()
  return db.todos.findMany()
}, 'todos')

// Server action (mutations)
const addTodo = action(async (formData: FormData) => {
  'use server'
  const text = formData.get('text') as string
  const db = await getDatabase()
  await db.todos.create({ data: { text, completed: false } })
  throw redirect('/todos') // revalidates
})

export default function TodosPage() {
  const todos = createAsync(() => getTodos())

  return (
    <div>
      <form action={addTodo} method="post">
        <input name="text" placeholder="New todo" required />
        <button type="submit">Add</button>
      </form>
      <Suspense fallback={<p>Loading...</p>}>
        <For each={todos()}>
          {(todo) => <p>{todo.text}</p>}
        </For>
      </Suspense>
    </div>
  )
}

Testes

Configuração do Vitest

npm install -D vitest @solidjs/testing-library @testing-library/jest-dom jsdom
npm install -D vite-plugin-solid
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import solid from 'vite-plugin-solid'

export default defineConfig({
  plugins: [solid()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/test-setup.ts',
    transformMode: { web: [/\.[jt]sx?$/] },
  },
})

Testando Componentes

import { render, screen, fireEvent } from '@solidjs/testing-library'
import { describe, it, expect } from 'vitest'
import Counter from './Counter'

describe('Counter', () => {
  it('renders initial count', () => {
    render(() => <Counter initialCount={5} />)
    expect(screen.getByText('Count: 5')).toBeInTheDocument()
  })

  it('increments on click', async () => {
    render(() => <Counter initialCount={0} />)
    const button = screen.getByRole('button', { name: /increment/i })
    fireEvent.click(button)
    expect(screen.getByText('Count: 1')).toBeInTheDocument()
  })
})

Boas Práticas

  1. Sempre chame signals como funçõescount() e não count. Esquecer os parênteses é o erro mais comum no SolidJS; sem eles você passa a função getter em vez do valor.

  2. Não desestruture props — a desestruturação quebra a reatividade porque lê o valor apenas uma vez. Use props.name diretamente ou use splitProps()/mergeProps() para manipulação de props.

  3. Use <For> para arrays de objetos, <Index> para primitivos<For> chaveia por referência (melhor para objetos que podem reordenar) enquanto <Index> chaveia por posição (melhor para listas de valores simples).

  4. Prefira stores para estado complexo — signals são ótimos para valores simples, mas para objetos aninhados e arrays, createStore fornece reatividade granular sem substituir o objeto inteiro.

  5. Use on() para rastreamento explícito — quando quiser que um efeito rastreie apenas signals específicos (não todos os signals lidos internamente), envolva com on(signal, callback).

  6. Aproveite Suspense e ErrorBoundary — envolva componentes assíncronos em Suspense para estados de carregamento e ErrorBoundary para tratamento elegante de erros.

  7. Carregue rotas com lazy-loading — use lazy(() => import('./Page')) para componentes de rota a fim de habilitar code splitting e reduzir o tamanho do bundle inicial.

  8. Use batch() para múltiplas atualizações — ao definir vários signals de uma vez, envolva-os em batch() para disparar uma única re-renderização em vez de múltiplas.

  9. Mantenha os componentes pequenos — componentes SolidJS executam seu corpo apenas uma vez (não a cada renderização como no React), então divida componentes grandes para limitar o escopo das atualizações reativas.

  10. Use generics TypeScript com signalscreateSignal<string | null>(null) fornece melhor segurança de tipos e autocomplete para seu estado reativo.

  11. Entenda o modelo de compilação — SolidJS compila JSX em operações DOM reais em tempo de compilação. Não há diffing de DOM virtual, e é por isso que é rápido, mas também por que as regras de reatividade (sem desestruturação, chamar signals) importam.

  12. Use untrack() com moderação — ler um signal dentro de untrack() impede o rastreamento de dependência. Isso é útil para logging ou leituras únicas, mas pode causar bugs se usado em excesso.