Saltar al contenido

Estado global y SSR

En el navegador un módulo dura lo que la pestaña: su ámbito coincide con un solo usuario. En un servidor SSR el mismo módulo se evalúa una vez por proceso y se comparte entre todas las peticiones, así que un singleton de módulo con estado por usuario filtra datos de un cliente a otro. El patrón correcto en SolidStart es crear ese estado dentro del árbol —en la raíz, por contexto— para que cada petición tenga el suyo.

Un módulo se comparte entre peticiones

El sistema de módulos evalúa el cuerpo de un archivo la primera vez que se importa y reutiliza sus exportaciones para siempre. En el cliente eso te dio el singleton reactivo gratis: una pestaña, un módulo, un usuario. En el servidor, la misma mecánica se vuelve una trampa. El proceso que sirve tu aplicación evalúa el módulo una vez al arrancar y lo mantiene vivo mientras dure el proceso, atendiendo con esas mismas variables a cada petición que llega.

TypeScript
// store/sesion.ts  —  PELIGROSO EN EL SERVIDORimport { createSignal } from "solid-js";const [usuario, setUsuario] = createSignal<string | null>(null);export { usuario };export const entrar = (nombre: string) => setUsuario(nombre);

Imagina dos peticiones casi simultáneas. La de Ana renderiza, llama a entrar("Ana") y usuario() pasa a "Ana". Un instante después llega la de Beto; su render lee usuario() y encuentra… "Ana", porque usuario es la misma variable para ambos. El estado de un usuario se ha filtrado en la respuesta de otro. No es un caso raro de concurrencia exótica: es el comportamiento normal de una variable de módulo en un proceso que sirve a muchos, y basta cualquier solapamiento de peticiones para dispararlo.

Y ojo: darle una raíz con createRoot, como hicimos para el ciclo de vida en el cliente, no arregla esto. Un createRoot en el ámbito de módulo también se ejecuta una vez por proceso; su dispose gobierna cuándo muere el estado, no entre quiénes se comparte mientras vive. El problema no es la falta de un interruptor de apagado, sino que el estado es único para un proceso que atiende a muchos.

La regla: el estado por petición vive en el árbol

La solución invierte el instinto que traías del cliente. Allí sacabas el estado fuera del árbol de componentes para hacerlo global; aquí, para que sea correcto por petición, tienes que meterlo dentro del árbol. La razón es que, en SSR, el árbol de componentes se re-renderiza en cada petición: cada solicitud construye su propio árbol desde la raíz. Cualquier estado creado dentro de ese árbol nace, por tanto, una vez por petición y muere con ella. Ese es justo el alcance que el estado por usuario necesita.

El vehículo natural para distribuir ese estado por el árbol sin pasarlo por props es el contexto —y aquí, como adelantó la lección anterior, deja de ser opcional—. Creas el store dentro de un componente proveedor cerca de la raíz; como el proveedor se ejecuta una vez por petición, cada petición obtiene una instancia fresca y aislada.

TSX
// SesionProvider.tsx  —  una instancia por peticionimport { createSignal, useContext, createContext, type JSX } from "solid-js";const SesionCtx = createContext<ReturnType<typeof crearSesion>>();function crearSesion() {  const [usuario, setUsuario] = createSignal<string | null>(null);  return {    get usuario() { return usuario(); },    entrar: (n: string) => setUsuario(n),    salir: () => setUsuario(null),  };}export function SesionProvider(props: { children: JSX.Element }) {  const sesion = crearSesion();                 // se ejecuta por peticion en SSR  return <SesionCtx.Provider value={sesion}>{props.children}</SesionCtx.Provider>;}export const usarSesion = () => useContext(SesionCtx)!;

La factory crearSesion es exactamente el patrón de la lección anterior; lo único que cambia es dónde se la llama: dentro de un proveedor del árbol, no en la cima de un módulo. Ese pequeño desplazamiento —de ámbito de módulo a ámbito de componente— es toda la diferencia entre un estado que se comparte entre usuarios y uno que se aísla por petición.

El patrón correcto en SolidStart

En SolidStart el sitio de ese proveedor es la raíz de la aplicación, típicamente en src/app.tsx, envolviendo el árbol de rutas. Como el componente raíz se ejecuta en cada petición del servidor, el store que crea el proveedor es nuevo por petición en el servidor y único por pestaña en el cliente —el mismo código sirve a ambos mundos, que es justo lo que la factory prometía—.

TSX
// src/app.tsximport { Router } from "@solidjs/router";import { FileRoutes } from "@solidjs/start/router";import { Suspense } from "solid-js";import { SesionProvider } from "./SesionProvider";export default function App() {  return (    <Router      root={(props) => (        <SesionProvider>          <Suspense>{props.children}</Suspense>        </SesionProvider>      )}    >      <FileRoutes />    </Router>  );}

Para estado que solo vive en el servidor —no reactivo, atado a la petición HTTP en sí, como el usuario autenticado que resuelve un middleware— SolidStart ofrece además getRequestEvent, que devuelve el evento de la petición actual con un locals donde depositar datos por petición sin tocar variables de módulo.

TypeScript
import { getRequestEvent } from "solid-js/web";// En codigo de servidor: leer datos por peticion, nunca de un modulo compartido.export function usuarioActual() {  const evento = getRequestEvent();  return evento?.locals.usuario ?? null;       // aislado por peticion}

Y para datos que cargas de forma asíncrona, los primitivos del framework —query del router y createAsync— ya están diseñados para deduplicar y cachear dentro del alcance de una petición, no entre ellas. Si usas esas primitivas para tu estado de datos en lugar de un singleton casero, el aislamiento por petición te lo da el framework sin que tengas que pensarlo. La disciplina, en resumen, es una sola: en SSR, el estado por usuario nunca vive en un módulo; vive en el árbol, en la petición o en las primitivas que ya respetan su alcance.

Qué sí puede ser global al proceso

La regla no prohíbe todo estado de módulo en el servidor; prohíbe el estado por usuario. Hay categorías enteras de estado para las que ser único por proceso no es un defecto sino la intención: precisamente porque no pertenecen a ningún usuario, compartirlas entre peticiones es correcto y hasta deseable.

El criterio que separa lo permitido de lo prohibido es limpio: pregúntate si el valor pertenece a un usuario. Si la respuesta es sí —su sesión, su carrito, sus permisos, cualquier cosa que cambie según quién pide—, ese estado tiene que vivir por petición. Si la respuesta es no —una constante, un recurso de infraestructura, datos públicos iguales para todos—, entonces ser global al proceso es exactamente lo que quieres, y un módulo es su hogar correcto.