De 1.x a 2.0: qué se rompe, qué se mantiene y cómo migrar
El inventario honesto del salto de versión mayor. Qué permanece idéntico —el modelo mental, los signals, el control flow, el JSX—, qué cambia de forma observable —batching automático, timing de efectos afinado, createAsync ascendido al núcleo, el store con projections—, y la estrategia de migración que hace el salto barato: adoptar hoy los patrones que ya son 2.0, migrar por hojas con la capa de compatibilidad, y no reescribir lo que no se rompe.
Lo que se mantiene: el modelo mental intacto
Empecemos por la buena noticia, que es la mayor parte. El modelo mental de Solid no cambia en 2.0: el componente sigue corriendo una vez, la reactividad sigue siendo de grano fino, no aparece un virtual DOM ni un ciclo de render. createSignal, createMemo, createEffect conservan su forma y su semántica de lectura. El control flow —Show, For, Index, Switch, Dynamic, Portal— se usa igual. El JSX compila igual, y las reglas de oro —leer llamando, no destructurar props, onCleanup atado al dueño— siguen vigentes palabra por palabra. Suspense y ErrorBoundary son las mismas fronteras, ahora alimentadas por más del grafo.
Esto no es casualidad ni suerte: es consecuencia de que 2.0 reescribe el runtime, no la API de superficie. La reactividad por debajo es otra —con nodos que saben estar pendientes, como viste en el capítulo del runtime—, pero la piel que tocas como programador se preservó a propósito, porque la coherencia del modelo era ya su mayor activo. Migrar Solid no es reaprender Solid.
// Este componente es byte por byte idéntico en 1.x y en 2.0function Contador() { const [n, setN] = createSignal(0); const doble = createMemo(() => n() * 2); return ( <Show when={n() >= 0} fallback={<p>negativo</p>}> <button onClick={() => setN(n() + 1)}>{doble()}</button> </Show> );}Si la mayor parte de tu base de código se parece a esto —signals, memos, control flow, JSX—, la mayor parte de tu base de código no cambia. Ese es el tamaño real de la migración: no el de la reescritura del motor, sino el del puñado de puntos donde el motor nuevo se comporta de forma observable.
Lo que cambia: el inventario de rupturas
Las rupturas existen, pero son pocas y localizadas. Conviene tenerlas en una tabla mental antes que en la sorpresa de un bug.
| Área | 1.x | 2.0 | Impacto |
|---|---|---|---|
| Batching | Manual con batch en varios casos |
Automático por el planificador | batch pasa a redundante |
| Timing de efectos | Semántica de flush con matices | Planificado y más predecible | Efectos que dependían del orden fino |
| Datos async | createResource como norma |
createAsync ascendido al núcleo |
Cambia el patrón por defecto |
| Store | Store clásico | Reescrito con projections | API de estado derivado async |
| Transiciones | Añadido sobre el runtime | Parte del núcleo | Semántica más uniforme |
Ninguna de estas filas te obliga a reescribir un componente entero. La mayoría son cambios que simplifican —quitas un batch que ya no hace falta, escribes createAsync en vez de orquestar un recurso— y las que requieren cuidado son las de timing, porque un código que se apoyaba en el orden exacto de ejecución de efectos en 1.x puede observar un orden distinto en 2.0. Ese es el punto donde conviene tener pruebas.
Hay una asimetría reconfortante en esta tabla: casi todas las rupturas se resuelven borrando código, no escribiéndolo. Migrar aquí no es el trabajo habitual de un salto mayor —reescribir para adaptarse a APIs nuevas más ceremoniosas— sino su opuesto: retirar el andamiaje que el runtime nuevo vuelve innecesario. Cada batch que quitas, cada tríada de signals de carga que colapsas en un createAsync, cada comprobación de orden que sustituyes por una dependencia declarada, deja el código más corto y más claro que antes. Un salto de versión mayor que adelgaza tu base de código en vez de engordarla es una rareza que conviene aprovechar sin recelo.
// 1.x: recurso mas comprobaciones de estado repartidas por el JSXconst [datos] = createResource(id, cargar);// en la vista: datos.loading, datos.error, guardias contra undefined// 2.0: derivacion declarativa; carga y error suben a las fronterasconst datos = createAsync(() => cargar(id()));// carga -> Suspense ; error -> ErrorBoundary ; valor -> datos()Batching automático y timing de efectos
Merece detenerse en el cambio más visible en el día a día: el batching automático. En 1.x, varias escrituras sueltas podían disparar varias propagaciones, y usabas batch para agruparlas en una sola. En 2.0 el planificador agrupa por defecto: escribas donde escribas dentro de un mismo tick, la propagación ocurre una vez al final. batch no desaparece —sigue exportándose por compatibilidad— pero se vuelve, en la práctica, un no-op que ya no necesitas escribir.
// 1.x: sin batch, dos propagaciones; con batch, unabatch(() => { setNombre("Ada"); setApellido("Lovelace");});// 2.0: el planificador agrupa solo; batch sobrasetNombre("Ada");setApellido("Lovelace"); // una sola propagacion al final del tickEl corolario en efectos es que su ejecución se vuelve más predecible y menos sensible a cuántas escrituras la precedieron. Un efecto que en 1.x podía correr dos veces ante dos escrituras seguidas, en 2.0 corre una vez con el estado ya consolidado. Esto es casi siempre lo que querías; el único código que lo nota mal es el que —de nuevo— contaba con el orden o la frecuencia exacta de ejecución en lugar de con sus dependencias declaradas.
Conviene subrayar qué no cambia entre las utilidades de control fino, para que la migración no siembre miedos infundados. untrack sigue leyendo sin suscribirse, on sigue declarando dependencias explícitas con su opción defer, y getOwner con runWithOwner siguen gobernando el árbol de dueños igual que antes. Lo que se vuelve redundante es solo batch, porque su trabajo lo asume el planificador; el resto del vocabulario de reactividad fina permanece intacto y con la misma semántica. La regla mental es que 2.0 te quita una herramienta que ya no necesitas, no te obliga a aprender diez nuevas.
// on y untrack no cambian: siguen siendo la forma de controlar el rastreocreateEffect(on(fuente, (v) => registrar(v), { defer: true }));const instantanea = untrack(() => contador()); // lee sin suscribir, igual en 2.0Estrategia de migración: incremental y por hojas
La migración no se hace de golpe ni se hace a ciegas. Se hace incremental, por hojas y apoyada en la capa de compatibilidad que 2.0 ofrece para que 1.x y los patrones nuevos convivan durante la transición. El plan tiene un orden natural.
Primero, adopta hoy lo que ya es 2.0: escribe datos con createAsync y query en vez de con createEffect más signals de estado, y deja de apoyarte en batch donde el agrupamiento automático lo hará por ti. Cada componente que escribas así hoy es uno que no tendrás que tocar mañana. Segundo, cuando el salto llegue, migra por hojas: convierte primero los componentes de hoja —los que nadie depende de ellos— y sube por el árbol, porque un fallo en una hoja es fácil de aislar. Tercero, fija versiones y prueba el timing: bloquea la versión durante la migración, ejecuta tu suite y presta atención especial a los efectos que dependían del orden, que son los únicos candidatos reales a romperse.
El proceso se apoya en herramienta, no solo en disciplina. El equipo de Solid publica una guía de migración con la lista exacta de cambios y, donde es posible, codemods que reescriben los patrones mecánicos —los que se pueden transformar sin criterio humano— dejándote a ti solo los que requieren juicio. La capa de compatibilidad permite además que un mismo proyecto tenga módulos ya migrados conviviendo con módulos aún en 1.x, de modo que no hay un día del big bang en que todo debe funcionar a la vez. Migras un trozo, lo pruebas, lo despliegas, y sigues; el riesgo se reparte en muchos pasos pequeños en lugar de concentrarse en uno grande.
La regla que gobierna todo el proceso es simple y liberadora: no reescribas lo que no se rompe. La tentación en un cambio mayor es aprovechar para reescribir todo con el estilo nuevo; resístela. Lo que funciona en 1.x y no toca ninguna de las filas de la tabla de rupturas seguirá funcionando en 2.0 sin cambios, y reescribirlo solo introduce riesgo sin beneficio. La migración barata es la que toca lo mínimo: los pocos puntos que de verdad cambian, y ni uno más.
La disciplina de tocar lo mínimo también protege tu historial de versiones: un diff pequeño y quirúrgico se revisa, se prueba y se revierte con facilidad si algo falla, mientras que una reescritura masiva mezcla la migración con cambios de estilo y vuelve imposible saber qué introdujo un bug. Separa siempre la migración de la refactorización estética; son dos trabajos distintos y juntarlos multiplica el riesgo de ambos sin ganar nada.