wandres.dev
FILE ROUTING · rutas y layouts

Rutas anidadas y layouts: envolver con props.children

Las carpetas anidan segmentos de URL, y un fichero hermano con el mismo nombre que una carpeta se convierte en su layout: un componente que envuelve a todas las rutas hijas y pinta el hijo activo con props.children. Las rutas index sirven la ruta de la carpeta en sí, y los layouts persisten entre navegaciones hermanas sin remontarse, conservando estado y DOM. Aquí se ve el detalle sutil de que blog index no es lo mismo que blog layout.

⏱ 16 min

Anidar rutas es anidar carpetas, pero la pieza que da poder a la anidación es el layout: un fichero que envuelve a todo un grupo de rutas y decide dónde aparece la que está activa. En SolidStart ese fichero se declara con una convención de nombres elegante —un hermano que se llama igual que la carpeta— y su único deber es pintar props.children. Esta lección desgrana cómo se componen los layouts en cadena, qué hace exactamente una ruta index, y por qué un layout sobrevive intacto mientras sus hijos entran y salen.

🎯 Al terminar esta lección sabrás
  • Anidar segmentos de URL creando subcarpetas dentro de routes/.
  • Declarar un layout con el fichero hermano homónimo y pintar el hijo con props.children.
  • Distinguir la ruta index de una carpeta de su fichero de layout.
  • Entender que los layouts persisten entre navegaciones hermanas y por qué eso importa.

Carpetas que anidan segmentos

Cada subcarpeta dentro de routes/ añade un segmento a la URL. Una carpeta blog/ con ficheros dentro produce rutas bajo /blog/..., y el fichero index de esa carpeta responde exactamente a /blog. Hasta aquí es pura estructura de directorios; lo interesante aparece cuando queremos que todas esas rutas compartan una envoltura común —una barra lateral, un encabezado de sección— sin repetirla en cada página.

routes/
  blog.tsx          <- layout de /blog/*
  blog/
    index.tsx       <- /blog
    solid.tsx       <- /blog/solid
    astro.tsx       <- /blog/astro

El fichero hermano que envuelve

Para crear un layout, escribes un fichero con el mismo nombre que la carpeta, a su lado. Ese blog.tsx no es una página más: se convierte en el envoltorio de todo lo que vive dentro de blog/. Recibe la página hija en props.children y decide dónde renderizarla. El tipo RouteSectionProps de @solidjs/router te da ese children ya tipado.

// src/routes/blog.tsx
import type { RouteSectionProps } from "@solidjs/router";

export default function LayoutBlog(props: RouteSectionProps) {
  return (
    <div class="blog">
      <aside>
        <h2>Secciones</h2>
        <nav>...</nav>
      </aside>
      <main>{props.children}</main>
    </div>
  );
}

El props.children es el hueco por donde entra la ruta activa: cuando el usuario está en /blog/solid, ahí se pinta el componente de blog/solid.tsx; cuando pasa a /blog/astro, solo cambia lo que hay en ese hueco. La <aside> permanece. Los layouts se componen en cadena: si dentro de blog/ hubiera otra carpeta con su propio layout, tendrías dos capas, cada una pintando su props.children, reflejando exactamente la jerarquía de segmentos de la URL.

La cadena se ve mejor con dos niveles: un layout de sección y, dentro, un sublayout que a su vez expone su hueco.

routes/
  panel.tsx              <- layout externo de /panel/*
  panel/
    ajustes.tsx          <- layout interno de /panel/ajustes/*
    ajustes/
      perfil.tsx         <- /panel/ajustes/perfil
      seguridad.tsx      <- /panel/ajustes/seguridad

En /panel/ajustes/perfil, panel.tsx envuelve a ajustes.tsx, que envuelve a perfil.tsx, cada capa pintando la siguiente en su props.children. La anidación de carpetas se ha vuelto anidación de componentes, segmento a segmento.

// src/routes/panel/ajustes.tsx — sublayout de /panel/ajustes
import type { RouteSectionProps } from "@solidjs/router";

export default function LayoutAjustes(props: RouteSectionProps) {
  return (
    <section class="ajustes">
      <nav>Perfil · Seguridad</nav>
      <div class="panel-ajustes">{props.children}</div>
    </section>
  );
}
flowchart TD
ROOT[root de la app] --> BL[layout blog]
BL -->|props children pinta el hijo| IDX[indice del blog]
BL -->|props children pinta el hijo| SOL[articulo solid]
BL -->|props children pinta el hijo| AST[articulo astro]
style BL fill:#89b4fa,color:#11111b
style IDX fill:#a6e3a1,color:#11111b
style SOL fill:#a6e3a1,color:#11111b
style AST fill:#a6e3a1,color:#11111b

Rutas index y un matiz que confunde

Aquí está el detalle que atrapa a casi todo el mundo: blog/index.tsx y blog.tsx no son lo mismo. El fichero index sirve una sola ruta, la de la carpeta en sí —/blog—, y es una página normal, no una envoltura. El layout es el hermano homónimo blog.tsx, y es quien envuelve a todas las rutas de la carpeta, incluida el propio index. Quien crea blog/index.tsx esperando que envuelva a los hijos descubre que no pasa nada: el index no ve a sus vecinos, solo se pinta a sí mismo en /blog.

⚠️
index no es el layout

Si quieres una envoltura común para /blog, /blog/solid y /blog/astro, el fichero debe ser blog.tsx —el hermano de la carpeta—, no blog/index.tsx. El index es simplemente la página que responde a la ruta de la carpeta. Un truco de legibilidad: como tener muchos ficheros llamados index.tsx complica buscar, puedes renombrar el index al nombre de su carpeta entre paréntesis, por ejemplo blog/(blog).tsx, y seguirá sirviendo /blog sin añadir segmento. El paréntesis marca “esto es el índice, no un segmento nuevo”.

Los layouts persisten

La consecuencia más valiosa de esta arquitectura es que un layout no se remonta cuando navegas entre sus hijos. Al ir de /blog/solid a /blog/astro, LayoutBlog sigue siendo la misma instancia: su estado local, su scroll, sus efectos, todo sobrevive. Solo cambia el contenido de props.children. Esto es la reactividad de grano fino aplicada al enrutado: el router no reconstruye el árbol, sustituye la hoja que cambió.

// src/routes/blog.tsx
import { createSignal } from "solid-js";
import type { RouteSectionProps } from "@solidjs/router";

export default function LayoutBlog(props: RouteSectionProps) {
  // Este estado sobrevive a la navegacion entre articulos:
  const [abierta, setAbierta] = createSignal(true);
  return (
    <div classList={{ blog: true, "con-panel": abierta() }}>
      <aside>
        <button onClick={() => setAbierta((v) => !v)}>Alternar panel</button>
      </aside>
      <main>{props.children}</main>
    </div>
  );
}

El panel que el usuario abrió no se cierra al saltar de un artículo a otro, porque el layout nunca desapareció. Comparado con un modelo que desmonta y remonta la pantalla entera en cada navegación, aquí conservas foco, animaciones en curso y peticiones que el layout tuviera vivas. La regla mental es limpia: lo que comparten dos rutas hermanas, ponlo en su layout, y persistirá gratis.

Esa persistencia se extiende a los datos. Un layout es también el lugar donde declarar la carga que sus hijos comparten: si /blog/solid y /blog/astro necesitan la lista de categorías, la pides una vez en blog.tsx y sobrevive a la navegación entre artículos sin recargarse. Los layouts exponen además un route con una función preload para calentar esos datos antes de que el hijo se monte, encadenando persistencia y precarga en el mismo nivel del árbol.

💡
El layout es el nivel donde vive lo compartido

Cuando dudes dónde poner un estado, una suscripción de tiempo real o una carga de datos, pregúntate qué rutas lo comparten y súbelo al layout común de todas ellas. Como el layout no se remonta mientras navegas entre sus hijos, todo lo que declares ahí se declara una vez y persiste: un preload en su route, una conexión abierta, el estado de un panel lateral. El árbol de layouts se convierte así en una jerarquía de ámbitos de persistencia perfectamente alineada con la jerarquía de URLs.

🧅

Layout = hermano

equipo.tsx envuelve a todo equipo/. El nombre homónimo es la convención; props.children es el hueco.

🎯

index = página

equipo/index.tsx responde a /equipo. Es una hoja, no una envoltura; puede renombrarse a (equipo).tsx.

♻️

Persistencia

El layout no se remonta entre hijos hermanos. Estado, scroll y efectos sobreviven a la navegación.

El árbol de props.children es el espejo reactivo de la URL

Lo que de verdad enseña este nivel es que el enrutado en Solid no es un sistema aparte, sino la misma reactividad de grano fino proyectada sobre la barra de direcciones. Un layout es un componente que corre una vez y expone un hueco —props.children— por donde el router hace fluir, como si fuera un signal, la ruta activa. Cuando la URL cambia dentro del alcance de ese layout, no se reconstruye el árbol: se sustituye únicamente el contenido de ese hueco, igual que un efecto actualiza solo el nodo del DOM del que depende. De ahí nacen las tres verdades de la lección, que en realidad son una sola vista desde ángulos distintos. La anidación de carpetas es la anidación de layouts, porque cada nivel expone su propio props.children y los encadena en el orden exacto de los segmentos de la URL. La ruta index es una hoja y no una envoltura, porque el layout es siempre el hermano homónimo, no el índice interior. Y la persistencia no es una optimización que actives, sino la consecuencia directa de que el layout nunca se remonta: al conservar su identidad, conserva su estado. Interiorizar esto cambia cómo diseñas una aplicación entera. Dejas de preguntarte “dónde guardo este estado para que sobreviva a la navegación” y empiezas a preguntarte “en qué nivel de layout vive lo que estas rutas comparten”, porque el árbol de layouts es, literalmente, el mapa de qué permanece y qué cambia cuando el usuario se mueve.

⚔️ Compón layouts que persisten
  1. Crea routes/blog.tsx como layout con una barra lateral y routes/blog/index.tsx, blog/solid.tsx, blog/astro.tsx como páginas; navega entre ellas y observa que la barra permanece.
  2. Mete un createSignal en el layout que controle un panel; ábrelo, navega entre artículos y confirma que no se cierra.
  3. Convierte por error el layout en blog/index.tsx y comprueba que deja de envolver a los hijos: solo se pinta en /blog.
  4. Renombra el índice a blog/(blog).tsx y verifica que sigue sirviendo /blog sin añadir segmento.
  5. Anida un segundo nivel con su propio layout dentro de blog/ y traza la cadena de props.children que refleja los segmentos de la URL.