Crear un proyecto Solid: degit, create-solid y render()
Los dos caminos canónicos para andamiar Solid en 2026 —degit sobre solidjs/templates y el asistente npm create solid—, la plantilla solid-ts de Vite, la anatomía de un proyecto mínimo, y por qué el punto de entrada render() recibe una función y no un elemento.
Dos caminos para andamiar
El camino histórico y más ligero es degit, la utilidad de Rich Harris que clona un repositorio sin su historial de git. Las plantillas oficiales viven en el repositorio solidjs/templates, y cada subcarpeta es un punto de partida distinto —js, ts, ts-router, y variantes con Tailwind o UnoCSS—.
# clona la plantilla TypeScript, sin historia previanpx degit solidjs/templates/ts mi-appcd mi-appnpm installnpm run devEl camino moderno y recomendado en 2026 es el asistente oficial, que unifica bajo un mismo comando la creación de una app Solid pura o de un proyecto SolidStart. npm create solid@latest (equivalente a npm init solid@latest) hace unas pocas preguntas —TypeScript o JavaScript, plantilla, SSR sí o no— y andamia en consecuencia.
# asistente interactivo: Solid puro o SolidStart, TS o JSnpm create solid@latestExiste además una tercera vía perfectamente válida: la plantilla que mantiene el propio Vite. Es útil si ya piensas en clave Vite y quieres el mismo flujo que en otros frameworks.
# plantilla oficial de Vite para Solid con TypeScriptnpm create vite@latest mi-app -- --template solid-tsLa anatomía de un proyecto mínimo
Lo que copia la plantilla ts es deliberadamente escaso. No hay convenciones ocultas: cada fichero está porque alguien lo puso, y puedes borrarlo sin romper ninguna maquinaria secreta.
mi-app/├─ index.html # el host: contiene el div raiz y carga el modulo├─ src/│ ├─ index.tsx # punto de entrada: llama a render()│ ├─ App.tsx # el componente raiz de tu UI│ └─ index.css # estilos globales├─ vite.config.ts # registra vite-plugin-solid├─ tsconfig.json # jsx en modo preserve, jsxImportSource solid-js└─ package.json # scripts dev, build y serveEl index.html es un documento normal: un contenedor vacío y una etiqueta script de tipo módulo que arranca la aplicación. Vite intercepta esa carga y sirve el JSX ya compilado.
<!doctype html><html lang="es"> <head> <meta charset="utf-8" /> <title>Mi app Solid</title> </head> <body> <div id="root"></div> <script src="/src/index.tsx" type="module"></script> </body></html>La pieza que hace todo esto posible es vite-plugin-solid. Sin él, el navegador vería JSX crudo y fallaría; con él, Vite pasa cada .tsx por el preset de Babel de Solid, que transforma el JSX en operaciones de DOM reales antes de servirlo.
import { defineConfig } from "vite";import solid from "vite-plugin-solid";export default defineConfig({ plugins: [solid()],});Dos ficheros más completan el andamiaje y conviene abrirlos una vez. El tsconfig.json incluye la línea que hace que TypeScript entienda el JSX de Solid sin compilarlo él mismo: lo deja intacto con "jsx": "preserve" y apunta su origen a Solid con "jsxImportSource": "solid-js". Así TypeScript solo tipa; quien transforma el JSX es Babel, dentro del plugin.
{ "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js" }}El package.json aporta el vocabulario que teclearás a diario. Sus scripts son alias finos sobre Vite, no comandos propios de Solid:
npm run dev # servidor de desarrollo con recarga en calientenpm run build # build de produccion optimizado, a la carpeta distnpm run serve # sirve ese build para revisarlo antes de desplegarEl punto de entrada: render()
Aquí ocurre lo importante. render(), importado de solid-js/web, es la función que crea la raíz reactiva, ejecuta tu código dentro de ella e inserta el resultado en un elemento del DOM. Su firma tiene dos parámetros: el código que produce la UI y el contenedor donde montarla.
/* @refresh reload */import { render } from "solid-js/web";import App from "./App";import "./index.css";const root = document.getElementById("root");render(() => <App />, root!);El detalle que separa a quien entiende Solid de quien copia: el primer argumento es () => <App />, una función, no <App /> a secas. La razón es profunda. render() establece primero un contexto reactivo —un owner que gobierna efectos y limpieza— y solo entonces ejecuta el código que crea el JSX. Si le pasaras <App /> ya evaluado, el árbol se construiría fuera de esa raíz, y los efectos que naciesen ahí no tendrían dueño ni recibirían limpieza. Envolverlo en una función difiere su ejecución hasta estar dentro del contexto correcto.