SolidJS
Framework de UI JavaScript reactivo de grano fino sin DOM virtual y rendimiento excepcional.
Instalación
Sección titulada «Instalación»Crear Nuevo Proyecto
Sección titulada «Crear Nuevo Proyecto»| Comando | Descripción |
|---|---|
npx degit solidjs/templates/ts my-app | Crear proyecto SolidJS con TypeScript |
npx degit solidjs/templates/js my-app | Crear proyecto SolidJS con JavaScript |
npm create solid@latest | Crear proyecto con la CLI de SolidStart |
npm install solid-js | Instalar SolidJS en un proyecto existente |
npm install solid-start | Instalar el meta-framework SolidStart |
Comandos de Desarrollo
Sección titulada «Comandos de Desarrollo»| Comando | Descripción |
|---|---|
npm run dev | Iniciar servidor de desarrollo con hot reload |
npm run build | Compilar para producción |
npm run serve | Previsualizar compilación de producción localmente |
npm run start | Iniciar servidor SolidStart |
Plantillas de Proyecto
Sección titulada «Plantillas de Proyecto»# 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
Componentes y JSX
Sección titulada «Componentes y JSX»Componentes Básicos
Sección titulada «Componentes Básicos»| Comando | Descripción |
|---|---|
function App() { return <div>Hello</div> } | Definir un componente básico |
<MyComponent name="value" /> | Pasar props a un componente |
props.children | Acceder 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 |
Manejo de Eventos
Sección titulada «Manejo de Eventos»| Comando | Descripció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 |
Patrones de Componentes
Sección titulada «Patrones de Componentes»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>
}
Primitivos Reactivos
Sección titulada «Primitivos Reactivos»Signals
Sección titulada «Signals»| Comando | Descripció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 |
Effects y Memos
Sección titulada «Effects y Memos»| Comando | Descripció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 |
Ciclo de Vida
Sección titulada «Ciclo de Vida»| Comando | Descripció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 |
Patrones Reactivos
Sección titulada «Patrones Reactivos»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>
)
}
Flujo de Control
Sección titulada «Flujo de Control»Componentes Integrados
Sección titulada «Componentes Integrados»| Comando | Descripció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 |
Ejemplos de Flujo de Control
Sección titulada «Ejemplos de Flujo de Control»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>
)
}
Stores y Estado
Sección titulada «Stores y Estado»Operaciones de Store
Sección titulada «Operaciones de Store»| Comando | Descripció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 |
Patrones de Store
Sección titulada «Patrones 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' } },
})
// 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 (/* ... */)
}
Resource y Obtención de Datos
Sección titulada «Resource y Obtención de Datos»Operaciones de Resource
Sección titulada «Operaciones de Resource»| Comando | Descripció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.loading | Verificar si el resource está cargando |
data.error | Acceder al error del resource |
data.latest | Obtener último valor resuelto (sobrevive al refetch) |
data.state | Obtener estado del resource (‘unresolved’, ‘pending’, ‘ready’, ‘errored’) |
const { refetch } = data | Disparar refetch manualmente |
createResource(source, fetcher, { initialValue }) | Resource con valor inicial |
Patrones de Obtención de Datos
Sección titulada «Patrones de Obtención de Datos»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>
)
}
Enrutamiento
Sección titulada «Enrutamiento»@solidjs/router
Sección titulada «@solidjs/router»| Comando | Descripción |
|---|---|
npm install @solidjs/router | Instalar 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 |
Configuración del Router
Sección titulada «Configuración del Router»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>
)
}
Contexto e Inyección de Dependencias
Sección titulada «Contexto e Inyección de Dependencias»API de Contexto
Sección titulada «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
}
// 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>
)
}
Estilos
Sección titulada «Estilos»Opciones de CSS
Sección titulada «Opciones de CSS»| Comando | Descripción |
|---|---|
import styles from './App.module.css' | Importar CSS modules |
<div class={styles.container}> | Aplicar clase de CSS module |
npm install solid-styled-components | Instalar styled-components para Solid |
const Btn = styled('button')\color: red“ | Crear componente con estilos |
<div class="static-class"> | Aplicar clase CSS estática |
Integración con Tailwind CSS
Sección titulada «Integración con Tailwind CSS»# 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;
SolidStart (Meta-Framework)
Sección titulada «SolidStart (Meta-Framework)»Características de SolidStart
Sección titulada «Características de SolidStart»| Comando | Descripción |
|---|---|
npm create solid@latest | Crear 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 |
Funciones y Acciones del Servidor
Sección titulada «Funciones y Acciones del Servidor»// 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>
)
}
Testing
Sección titulada «Testing»Configuración de Vitest
Sección titulada «Configuración de 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?$/] },
},
})
Testing de Componentes
Sección titulada «Testing de 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()
})
})
Mejores Prácticas
Sección titulada «Mejores Prácticas»-
Siempre llama a los signals como funciones —
count()nocount. Olvidar los paréntesis es el error más común en SolidJS; sin ellos pasas la función getter en lugar del valor. -
No desestructures las props — la desestructuración rompe la reactividad porque lee el valor una sola vez. Usa
props.namedirectamente o usasplitProps()/mergeProps()para manipulación de props. -
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). -
Prefiere stores para estado complejo — los signals son geniales para valores simples, pero para objetos anidados y arrays,
createStoreproporciona reactividad granular sin reemplazar el objeto completo. -
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 conon(signal, callback). -
Aprovecha
SuspenseyErrorBoundary— envuelve componentes asíncronos enSuspensepara estados de carga yErrorBoundarypara manejo elegante de errores. -
Carga diferida para rutas — usa
lazy(() => import('./Page'))para componentes de ruta para habilitar code splitting y reducir el tamaño del bundle inicial. -
Usa
batch()para múltiples actualizaciones — cuando establezcas varios signals a la vez, envuélvelos enbatch()para disparar un solo re-render en lugar de múltiples. -
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.
-
Usa genéricos de TypeScript con signals —
createSignal<string | null>(null)proporciona mejor seguridad de tipos y autocompletado para tu estado reactivo. -
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.
-
Usa
untrack()con moderación — leer un signal dentro deuntrack()previene el rastreo de dependencias. Esto es útil para logging o lecturas únicas pero puede causar bugs si se usa en exceso.