Primitivos de Reactividad
Mapa completo de primitivos SolidJS
createSignal
Estado reactivo atómico. Getter + setter.
createEffect
Side effects. Se re-ejecuta con deps.
createMemo
Estado derivado. Cacheado automáticamente.
createStore
Estado complejo. Objetos y arrays anidados.
createResource
Async data fetching con Suspense.
createContext
Inyección de dependencias global.
Sin re-renders: Los componentes se ejecutan UNA sola vez. Sólo actualizan los nodos DOM exactos que dependen del signal cambiado. No existe el concepto de "re-render" de React.
Accede signals llamándolos: Siempre
count() — nunca count. Destructurar un signal rompe la reactividad.
Estado Reactivo Fundamental
createSignal — Estado Atómico
signal
import { createSignal } from "solid-js"; // Getter/setter pair — la base de toda reactividad const [count, setCount] = createSignal(0); // Leer: llamar como función console.log(count()); // 0 // Escribir: pasar valor o updater setCount(1); setCount((prev) => prev + 1); // Signal con objetos (CRM lead) const [lead, setLead] = createSignal({ name: "Acme Corp", status: "nuevo", valor: 12000 }); setLead((prev) => ({ ...prev, status: "calificado" })); // Uso en JSX — reactivo automáticamente function Counter() { const [n, setN] = createSignal(0); return ( <button onClick={() => setN(n() + 1)}> Leads: {n()} // n() en JSX = suscripción reactiva </button> ); }
Regla clave: Nunca destructures —
const { x } = state rompe la reactividad. Usa siempre state.x o createMemo.
createEffect — Side Effects
effect
import { createSignal, createEffect } from "solid-js"; const [query, setQuery] = createSignal(""); // Se ejecuta inmediatamente, luego al cambiar deps createEffect(() => { console.log("Buscando:", query()); // auto-tracking document.title = `CRM — ${query()}`; }); // Con cleanup — WebSocket del dashboard en tiempo real createEffect(() => { const ws = new WebSocket( `wss://api.cultivacrm.com/leads/${query()}` ); ws.onmessage = (e) => setLeads(JSON.parse(e.data)); // Cleanup: se ejecuta antes de reusar el efecto return () => ws.close(); }); // Efecto con condición — no rastrear condicionalmente createEffect(() => { const val = query(); // tracking ocurre aquí if (val.length > 2) { fetchLeads(val); // fuera del tracking } });
El auto-tracking detecta automáticamente qué signals se leen. No hay array de dependencias como en React.
createMemo — Estado Derivado
memo
import { createSignal, createMemo } from "solid-js"; // CRM: métricas derivadas del pipeline const [leads, setLeads] = createSignal([ { id: 1, valor: 5000, estado: "cerrado" }, { id: 2, valor: 12000, estado: "activo" }, { id: 3, valor: 8500, estado: "cerrado" }, ]); // Solo recalcula cuando leads() cambia const cerrados = createMemo(() => leads().filter((l) => l.estado === "cerrado") ); const totalCerrado = createMemo(() => cerrados().reduce((s, l) => s + l.valor, 0) ); const conversion = createMemo(() => ((cerrados().length / leads().length) * 100).toFixed(1) ); // En JSX — memos son signals también (llamar como fn) return ( <div> <p>Pipeline cerrado: €{totalCerrado()} <p>Conversión: {conversion()}% </div> );
Siempre prefiere createMemo sobre createEffect para valores derivados. El memo está cacheado — si las deps no cambian, devuelve el mismo valor sin recalcular.
createStore — Estado Complejo
store
import { createStore, produce } from "solid-js/store"; // CRM state — objetos y arrays anidados const [crm, setCRM] = createStore({ leads: [] as Lead[], loading: false, filtros: { estado: "todos", etapa: "pipeline" }, paginacion: { pagina: 1, totalPaginas: 10 }, }); // Actualizar propiedad anidada — ruta granular setCRM("loading", true); setCRM("filtros", "estado", "activo"); setCRM("leads", 0, "valor", 15000); // primer lead // produce() — mutaciones tipo Immer setCRM(produce((s) => { s.leads.push({ id: 42, name: "TechStart SL", valor: 18500, estado: "nuevo" }); s.loading = false; s.paginacion.pagina = 1; })); // Actualizar múltiples items con selector setCRM("leads", (l) => l.estado === "nuevo", "estado", "calificado" // batch update );
Store vs Signal: Usa
createStore para objetos/arrays con actualizaciones granulares. Usa createSignal para primitivos y objetos pequeños.
createResource — Async Data
resource
import { createSignal, createResource, Suspense } from "solid-js"; async function fetchLeadDetail(id: number) { const res = await fetch(`/api/leads/${id}`); if (!res.ok) throw new Error("Lead no encontrado"); return res.json(); } function LeadDetail() { const [leadId, setLeadId] = createSignal(1); // Re-fetch automático cuando leadId() cambia const [lead, { refetch, mutate }] = createResource( leadId, fetchLeadDetail ); return ( // Suspense integrado — sin lógica manual de loading <Suspense fallback={<Spinner />}> <Show when={!lead.error} fallback={<p>Error: {lead.error?.message}</p>} > <h1>{lead()?.nombre}</h1> <p>Valor: €{lead()?.valor}</p> <button onClick={refetch}>Actualizar</button> </Show> </Suspense> ); }
States:
lead.loading, lead.error, lead(). Llama a mutate() para actualizar optimistamente sin re-fetch.
Control Flow — Show / For / Switch
JSX
import { Show, For, Switch, Match, Index } from "solid-js"; // Show — condicional (no usar ternario con componentes) <Show when={isAdmin()} fallback={<ReadOnlyDashboard />} > <AdminPanel /> </Show> // For — lista con re-ordenamiento eficiente (keyed) <For each={leads()}> {(lead, i) => ( <LeadRow lead={lead} index={i()} // i es signal aquí /> )} </For> // Switch — múltiples ramas de estado del CRM <Switch fallback={<p>Estado desconocido</p>}> <Match when={estado() === "cargando"}> <Skeleton /> </Match> <Match when={estado() === "error"}> <ErrorBanner msg={error()}/> </Match> <Match when={estado() === "ok"}> <PipelineView /> </Match> </Switch> // NUNCA uses .map() en JSX — usa <For> // ❌ {leads().map(l => <Row lead={l} />)} // ✓ <For each={leads()}>{l => <Row lead={l} />}</For>
For vs Index:
<For> para listas con reordenamiento (CRM leads). <Index> cuando los items cambian en sitio pero no se reordenan (celdas de tabla fija).
Patrones Avanzados — Context y SolidStart SSR
createContext — Inyección de Dependencias
context
import { createContext, useContext, type ParentComponent } from "solid-js"; import { createStore } from "solid-js/store"; // CRM Auth Context type AuthCtx = { user: () => User | null; login: (u: User) => void; logout: () => void; }; const AuthContext = createContext<AuthCtx>(); // Provider — envuelve el árbol de componentes export const AuthProvider: ParentComponent = (props) => { const [state, setState] = createStore<{user: User|null}>( { user: null } ); const value: AuthCtx = { user: () => state.user, login: (u) => setState("user", u), logout: () => setState("user", null), }; return ( <AuthContext.Provider value={value}> {props.children} </AuthContext.Provider> ); }; // Hook para consumir — en cualquier componente hijo export const useAuth = () => useContext(AuthContext)!; // Uso en componente function NavBar() { const { user, logout } = useAuth(); return <p>Hola, {user()?.name}</p>; }
Mismo API que React
useContext. El ! en useContext(Ctx)! asume que siempre hay Provider — añade validación si no es seguro.
SolidStart — Full-Stack SSR
solidstart
// File-based routing: src/routes/leads/[id].tsx import { createAsync, useParams } from "@solidjs/router"; import { Show, Suspense } from "solid-js"; import { getLead } from "~/lib/leads"; // server fn export default function LeadPage() { const params = useParams(); const lead = createAsync(() => getLead(Number(params.id)) ); return ( <Suspense fallback={<Skeleton />}> <Show when={lead()}> {(l) => ( <article> <h1>{l().nombre}</h1> <p>Valor: €{l().valor}</p> </article> )} </Show> </Suspense> ); } // src/lib/leads.ts — sólo corre en el servidor "use server"; export async function getLead(id: number) { return db.query.leads.findFirst({ where: eq(leads.id, id) }); // Drizzle/Prisma — seguro, no expuesto } export async function updateLeadStatus( id: number, estado: string ) { // Server action — llámala desde cualquier componente return db.update(leads).set({ estado }) .where(eq(leads.id, id)); }
"use server" convierte funciones en RPCs seguros. El cliente nunca ve el código — acceso a DB, env vars y secretos completamente protegido.
Guia de Migración React → SolidJS
Equivalencias directas — React vs SolidJS
| React | SolidJS | Nota clave |
|---|---|---|
| useState(0) | createSignal(0) | Devuelve [getter, setter]. Leer: count(), no count |
| useEffect(() => {}, [dep]) | createEffect(() => { dep(); }) | Sin array de deps — auto-tracking por lectura de signals |
| useMemo(() => calc, [dep]) | createMemo(() => calc()) | También auto-tracking. El memo es un signal de solo lectura |
| useReducer | createStore | Actualizaciones granulares sin re-render del componente entero |
| useContext(Ctx) | useContext(Ctx) | API idéntico — createContext también idéntico |
| React.memo(Component) | No necesario | SolidJS no re-renderiza componentes — reactividad granular |
| items.map(i => <Row />) | <For each={items()}> | For hace diff eficiente. .map() recrea todos los nodos |
| useRef(null) | let el!: HTMLDivElement | O createSignal para refs reactivos. let el = asigna en onMount |
| React.lazy + Suspense | lazy() + <Suspense> | lazy() de solid-js — mismo patrón, integrado con createResource |
Reglas de Oro — Golden Rules SolidJS
Patrones correctos e incorrectos
✓
Llama los signals como función
count() en JSX, effects y memos — nunca count sin paréntesis
✕
No destructures signals
const { x } = state rompe el tracking. Usa state.x o createMemo
✓
Usa <For> para listas en JSX
Nunca .map() — For hace reconciliación eficiente keyed
✕
No esperes re-renders
Los componentes corren UNA vez. Toda la reactividad va dentro de JSX, effects o memos
✓
createStore para objetos/arrays complejos
Actualizaciones granulares sin clonar todo el estado
✕
No leas signals fuera del tracking scope
Leer un signal en setTimeout sin efecto no actualiza la UI
✓
createResource para async con Suspense
Integración automática — no manejes loading/error manualmente
✓
createMemo sobre createEffect para derivados
El memo está cacheado — el effect es para side effects reales
Rendimiento por defecto: SolidJS es ~10x más rápido que React en benchmarks de listas reactivas. El CRM con 500 leads en tiempo real se beneficia especialmente del DOM virtual eliminado.
SolidStart para el CRM: SSR con server functions (
"use server") permite acceso seguro a DB desde componentes. Cero boilerplate de API routes para queries simples.