Ir al contenido

SolidJS

Framework de UI JavaScript reactivo de grano fino sin DOM virtual y rendimiento excepcional.

ComandoDescripción
npx degit solidjs/templates/ts my-appCrear proyecto SolidJS con TypeScript
npx degit solidjs/templates/js my-appCrear proyecto SolidJS con JavaScript
npm create solid@latestCrear proyecto con la CLI de SolidStart
npm install solid-jsInstalar SolidJS en un proyecto existente
npm install solid-startInstalar el meta-framework SolidStart
ComandoDescripción
npm run devIniciar servidor de desarrollo con hot reload
npm run buildCompilar para producción
npm run servePrevisualizar compilación de producción localmente
npm run startIniciar servidor SolidStart
# SolidJS básico (Vite)
npx degit solidjs/templates/ts my-app
cd my-app && npm install

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

# Plantillas de la comunidad
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
ComandoDescripción
function App() { return <div>Hello</div> }Definir un componente básico
<MyComponent name="value" />Pasar props a un componente
props.childrenAcceder al contenido children
<div class={styles.container}>Aplicar clase CSS (usar class, no className)
<div classList={{ active: isActive() }}>Clases CSS condicionales
<div style={{ color: 'red', 'font-size': '14px' }}>Aplicar estilos inline
<div innerHTML={htmlString} />Establecer contenido HTML crudo
<div ref={myRef}>Adjuntar referencia al DOM
ComandoDescripción
<div onClick={handler}>Adjuntar evento delegado
<div on:click={handler}>Adjuntar evento DOM nativo (omite delegación)
<div onInput={(e) => setValue(e.target.value)}>Manejar eventos de input
<div on:keydown={(e) => handleKey(e)}>Manejar eventos de teclado nativamente
<div onClick={[handler, data]}>Pasar datos con el manejador de eventos
import { type Component, type ParentComponent } from 'solid-js'

// Componente tipado con 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>
  )
}

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

// Dividir props para reenvío
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}
    />
  )
}

// Fusionar props por defecto
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>
}
ComandoDescripción
const [count, setCount] = createSignal(0)Crear un signal reactivo
count()Leer valor del signal (debe llamarse como función)
setCount(5)Establecer signal a un valor específico
setCount(prev => prev + 1)Actualizar signal con valor anterior
const [name, setName] = createSignal<string>()Crear signal tipado
ComandoDescripción
createEffect(() => console.log(count()))Ejecutar efecto secundario al cambiar el signal
createEffect(on(count, (v) => log(v)))Rastrear signal específico explícitamente
createEffect(on([a, b], ([a, b]) => ...))Rastrear múltiples signals explícitamente
const double = createMemo(() => count() * 2)Crear valor derivado/calculado
createRenderEffect(() => ...)Efecto que se ejecuta antes del pintado del DOM
createComputed(() => ...)Efecto síncrono durante el renderizado
ComandoDescripción
onMount(() => { ... })Ejecutar una vez cuando el componente se monta
onCleanup(() => { ... })Ejecutar limpieza cuando el componente se desmonta
batch(() => { setA(1); setB(2) })Agrupar múltiples actualizaciones de signals
untrack(() => value())Leer signal sin rastrear dependencia
import { createSignal, createEffect, createMemo, on, batch, untrack } from 'solid-js'

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

  // Valor derivado (memoizado, recalcula solo cuando count cambia)
  const doubled = createMemo(() => count() * 2)
  const isEven = createMemo(() => count() % 2 === 0)

  // Efecto: se ejecuta cuando cualquier signal accedido cambia
  createEffect(() => {
    console.log(`Count is now: ${count()}`)
    // Rastrea automáticamente count() como dependencia
  })

  // Rastreo explícito con on()
  createEffect(on(count, (value, prev) => {
    console.log(`Changed from ${prev} to ${value}`)
  }, { defer: true })) // defer: omitir ejecución inicial

  // Agrupar actualizaciones (un solo re-render)
  const reset = () => batch(() => {
    setCount(0)
    setStep(1)
  })

  // Leer sin crear dependencia
  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>
  )
}
ComandoDescripción
<Show when={loggedIn()} fallback={<Login/>}>Renderizado condicional
<For each={items()}>{(item) => <li>{item}</li>}</For>Renderizar lista (con clave por referencia)
<Index each={items()}>{(item, i) => <li>{item()}</li>}</Index>Renderizar lista (con clave por índice)
<Switch><Match when={a()}>A</Match></Switch>Renderizado tipo switch/case
<ErrorBoundary fallback={err => <p>{err}</p>}>Capturar errores de renderizado
<Suspense fallback={<Loading/>}>Mostrar fallback durante carga asíncrona
<Dynamic component={MyComp} />Renderizar componente dinámico
<Portal mount={document.body}>Renderizar en nodo DOM diferente
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: renderizado condicional con fallback */}
      <Show when={user()} fallback={<p>Please log in</p>}>
        {(u) => <p>Welcome, {u().name}!</p>}
      </Show>

      {/* For: con clave por referencia (mejor para objetos) */}
      <For each={items()}>
        {(item, index) => (
          <div>
            <span>{index() + 1}. {item.name}</span>
            <span> ({item.status})</span>
          </div>
        )}
      </For>

      {/* Switch/Match: múltiples condiciones */}
      <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: capturar errores en el árbol hijo */}
      <ErrorBoundary fallback={(err, reset) => (
        <div>
          <p>Error: {err.message}</p>
          <button onClick={reset}>Try Again</button>
        </div>
      )}>
        <RiskyComponent />
      </ErrorBoundary>
    </div>
  )
}
ComandoDescripción
const [store, setStore] = createStore({})Crear un store reactivo
setStore('name', 'Alice')Actualizar propiedad del store por ruta
setStore('users', 0, 'name', 'Bob')Actualizar propiedad anidada del store
setStore('list', l => [...l, item])Agregar elemento al array del store
setStore('list', i => i.id === 1, 'done', true)Actualizar elementos coincidentes del array
produce(s => { s.count++ })Actualizaciones de store estilo mutable
reconcile(newData)Reemplazar datos del store eficientemente
unwrap(store)Obtener datos crudos sin proxy
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' } },
  })

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

  // Alternar elemento específico por coincidencia
  const toggleTodo = (id: number) => {
    setState('todos', todo => todo.id === id, 'completed', c => !c)
  }

  // Eliminar elemento (filter produce nuevo array)
  const deleteTodo = (id: number) => {
    setState('todos', todos => todos.filter(t => t.id !== id))
  }

  // Actualizaciones estilo mutable con produce
  const clearCompleted = () => {
    setState(produce((s) => {
      s.todos = s.todos.filter(t => !t.completed)
    }))
  }

  // Actualización anidada profunda
  const setTheme = (theme: string) => {
    setState('user', 'settings', 'theme', theme)
  }

  // Reemplazar datos completos del store (reconcile hace diff eficiente)
  const loadFromServer = async () => {
    const data = await fetch('/api/todos').then(r => r.json())
    setState('todos', reconcile(data))
  }

  return (/* ... */)
}
ComandoDescripción
const [data] = createResource(fetchFn)Crear resource asíncrono
const [data] = createResource(id, fetchFn)Resource con signal fuente reactivo
data()Acceder a datos del resource
data.loadingVerificar si el resource está cargando
data.errorAcceder al error del resource
data.latestObtener último valor resuelto (sobrevive al refetch)
data.stateObtener estado del resource (‘unresolved’, ‘pending’, ‘ready’, ‘errored’)
const { refetch } = dataDisparar refetch manualmente
createResource(source, fetcher, { initialValue })Resource con valor inicial
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 se recarga automáticamente cuando userId() cambia
  const [user, { refetch, mutate }] = createResource(userId, fetchUser)

  // Actualización optimista
  const updateName = async (newName: string) => {
    const prev = user()
    mutate({ ...prev!, name: newName }) // optimista
    try {
      await fetch(`/api/users/${userId()}`, {
        method: 'PATCH',
        body: JSON.stringify({ name: newName }),
      })
    } catch {
      mutate(prev) // revertir en caso de 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>
  )
}
ComandoDescripción
npm install @solidjs/routerInstalar el router de SolidJS
<Router><Route path="/" component={Home}/></Router>Definir ruta básica
<Route path="/users/:id" component={User}/>Ruta con parámetro
<Route path="/*all" component={NotFound}/>Ruta catch-all
const params = useParams()Acceder a parámetros de ruta
const [searchParams, setSearchParams] = useSearchParams()Acceder a parámetros de consulta
const navigate = useNavigate()Navegación programática
navigate('/dashboard')Navegar a una ruta
<A href="/about">About</A>Componente de enlace de navegación
<A href="/about" activeClass="active">Enlace con estilo activo
import { Router, Route, A, useParams, useNavigate, useSearchParams } from '@solidjs/router'
import { lazy } from 'solid-js'

// Rutas con carga diferida
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>
  )
}

// Rutas anidadas
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>
  )
}
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
}

// Uso
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>
  )
}
ComandoDescripción
import styles from './App.module.css'Importar CSS modules
<div class={styles.container}>Aplicar clase de CSS module
npm install solid-styled-componentsInstalar styled-components para Solid
const Btn = styled('button')\color: red“Crear componente con estilos
<div class="static-class">Aplicar clase CSS estática
# Instalar 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;
ComandoDescripción
npm create solid@latestCrear proyecto SolidStart
Directiva "use server"Marcar función como exclusiva del servidor
createServerAction$()Crear una acción del servidor
createRouteData()Cargar datos para una ruta en el servidor
<Title>Page Title</Title>Establecer título de página (de @solidjs/meta)
<Meta name="description" content="..." />Establecer meta tags
// src/routes/todos.tsx
import { createAsync, query, action, redirect } from '@solidjs/router'

// Consulta del servidor (carga de datos)
const getTodos = query(async () => {
  'use server'
  const db = await getDatabase()
  return db.todos.findMany()
}, 'todos')

// Acción del servidor (mutaciones)
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') // revalida
})

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>
  )
}
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?$/] },
  },
})
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()
  })
})
  1. Siempre llama a los signals como funcionescount() no count. Olvidar los paréntesis es el error más común en SolidJS; sin ellos pasas la función getter en lugar del valor.

  2. No desestructures las props — la desestructuración rompe la reactividad porque lee el valor una sola vez. Usa props.name directamente o usa splitProps()/mergeProps() para manipulación de props.

  3. Usa <For> para arrays de objetos, <Index> para primitivos<For> usa clave por referencia (mejor para objetos que pueden reordenarse) mientras que <Index> usa clave por posición (mejor para listas de valores simples).

  4. Prefiere stores para estado complejo — los signals son geniales para valores simples, pero para objetos anidados y arrays, createStore proporciona reactividad granular sin reemplazar el objeto completo.

  5. Usa on() para rastreo explícito — cuando quieras que un efecto rastree solo signals específicos (no todos los signals leídos dentro), envuélvelo con on(signal, callback).

  6. Aprovecha Suspense y ErrorBoundary — envuelve componentes asíncronos en Suspense para estados de carga y ErrorBoundary para manejo elegante de errores.

  7. Carga diferida para rutas — usa lazy(() => import('./Page')) para componentes de ruta para habilitar code splitting y reducir el tamaño del bundle inicial.

  8. Usa batch() para múltiples actualizaciones — cuando establezcas varios signals a la vez, envuélvelos en batch() para disparar un solo re-render en lugar de múltiples.

  9. Mantén los componentes pequeños — los componentes de SolidJS ejecutan su cuerpo solo una vez (no en cada render como React), así que divide componentes grandes para limitar el alcance de las actualizaciones reactivas.

  10. Usa genéricos de TypeScript con signalscreateSignal<string | null>(null) proporciona mejor seguridad de tipos y autocompletado para tu estado reactivo.

  11. Comprende el modelo de compilación — SolidJS compila JSX a operaciones reales del DOM en tiempo de compilación. No hay diffing de DOM virtual, por eso es rápido pero también por qué las reglas de reactividad (no desestructurar, llamar signals) importan.

  12. Usa untrack() con moderación — leer un signal dentro de untrack() previene el rastreo de dependencias. Esto es útil para logging o lecturas únicas pero puede causar bugs si se usa en exceso.