Saltar al contenido

Tipar resources y usarlos en SSR

createResource es genérico en el tipo del valor y el de la fuente, y su valor es honestamente T | undefined porque antes de resolver no hay dato: se estrecha leyéndolo dentro de Show o Suspense, no con un as. El tipo Resource<T> firma funciones que reciben resources ya creados. Y su diseño lo hace amistoso con el servidor: se integra con Suspense para el streaming, serializa su resultado en el HTML y rehidrata en el cliente sin volver a pedir. deferStream y ssrLoadFrom afinan ese comportamiento; el fetcher corre en el servidor, así que nada de window sin guardas.

Tipar el valor y la fuente

createResource es genérico en dos parámetros: T, el tipo del valor que resuelve, y S, el tipo de la fuente. TypeScript los infiere del fetcher y de la fuente, pero puedes fijarlos cuando la inferencia no basta.

TSX
import { createResource, createSignal } from "solid-js";interface Usuario { id: number; nombre: string }const [id] = createSignal(1);// T = Usuario, S = number, inferidos del fetcher y la fuenteconst [usuario] = createResource(id, async (idActual): Promise<Usuario> => {  const res = await fetch(`/api/usuario/${idActual}`);  return res.json();});usuario;   // Resource<Usuario>usuario(); // Usuario | undefined  <- el undefined no es opcional: es real

El detalle que no debes tapar con un as: el valor es T | undefined, no T. Ese undefined es la verdad del tipo —antes de que la promesa resuelva, no hay valor—, y TypeScript te obliga a tratarlo. La forma correcta de estrecharlo no es aseverar, sino leerlo dentro de un Show o un Suspense, que garantizan que el dato ya existe donde lo usas.

TSX
<Show when={usuario()}>  {(u) => <Perfil datos={u()} />}  {/* aqui u() es Usuario, sin undefined: Show ya lo estrecho */}</Show>

Si el undefined te estorba desde el principio porque tienes un valor por defecto sensato, la opción initialValue lo elimina del tipo: el resource arranca ya con ese valor y su firma pasa a ser T sin más.

TSX
const [lista] = createResource(traerLista, { initialValue: [] as Item[] });lista(); // Item[]  <- con initialValue, ya no hay undefined que estrechar

La disciplina general es resistir el atajo del as. Cada as Usuario sobre un resource sin resolver es una promesa que le haces al compilador y que el runtime puede romper: si el componente se renderiza antes de que el dato llegue, tu aserción miente y el undefined reaparece como un error en ejecución, justo donde jurabas que no podía haberlo. Estrechar con Show o Suspense no es ceremonia: es alinear la garantía del tipo con la garantía real de que el dato ya existe donde lo lees. El undefined del resource es información, no un estorbo que silenciar.

Este rigor paga dividendos en equipo. Un Resource<T> que viaja por props lleva su incertidumbre en el tipo, así que quien lo recibe no puede olvidar que el dato podría no estar aún. El sistema de tipos se vuelve documentación viva de que estás tratando con algo asíncrono, y esa honestidad —incómoda las primeras veces— es exactamente la que evita los errores de «leer una propiedad de undefined» que plagan las capas de datos mal tipadas de tantos proyectos.

Firmar funciones con Resource<T>

El tipo Resource<T> describe el accessor completo —el valor más loading, error, latest y state—, así que es lo que usas para tipar funciones o componentes que reciben un resource ya creado, en lugar de crearlo ellos.

TSX
import { type Resource } from "solid-js";function EstadoDe<T>(props: { recurso: Resource<T> }) {  return (    <Switch>      <Match when={props.recurso.loading}>Cargando</Match>      <Match when={props.recurso.error}>Hubo un error</Match>      <Match when={props.recurso()}>Listo</Match>    </Switch>  );}

Así separas quién crea el resource de quién lo consume: un componente puede recibir el Resource<T> por props y renderizar su ciclo de vida sin saber de dónde salió el dato. El tipo del segundo elemento de la tupla —el de refetch y mutate— también está disponible si necesitas pasar esas acciones por separado; el tipo de retorno completo de createResource es una tupla de Resource<T> y ese objeto de acciones.

Un apunte sobre la reactividad de las props: como en todo Solid, un componente que recibe un Resource<T> no debe destructurarlo. El resource es una función con propiedades reactivas, y separarlo en variables sueltas —const { loading } = props.recurso— congelaría esas lecturas y romperías el seguimiento. Accede siempre a través de props.recurso.loading, props.recurso(), y deja que cada lectura ocurra en el punto del JSX donde importa. La regla que aprendiste para las props ordinarias vale idéntica para los resources que viajan por ellas.

El tipo de todo lo que devuelve createResource tiene nombre propio, ResourceReturn<T>, y el de las acciones, ResourceActions<T>. Rara vez los escribes a mano —la inferencia basta—, pero conocerlos ayuda cuando construyes una abstracción que envuelve un resource y necesita re-exponer su tupla con precisión, sin recurrir a un any que borre las garantías que tanto cuidas.

Por qué el resource es amistoso con el servidor

Aquí está la razón por la que createResource es la base de la carga de datos en SolidStart: fue diseñado para el servidor desde el principio. Tres propiedades lo hacen amistoso con el SSR, y las tres operan sin que cambies tu código entre cliente y servidor.

Primero, se integra con Suspense. En el servidor, el renderizador espera a los resources que hay dentro de un límite de Suspense y va enviando su contenido a medida que resuelven, lo que habilita el SSR en streaming. Segundo, serializa su resultado: cuando el servidor resuelve un resource, Solid empaqueta el valor dentro del HTML que envía. Tercero, coordina la hidratación: en el cliente, el resource se rehidrata de ese valor serializado en lugar de volver a pedir, así que el primer render del cliente coincide con el HTML del servidor y no hay ni doble fetch ni parpadeo.

TSX
// El mismo codigo corre en servidor y en cliente:const [datos] = createResource(cargarDatos);// - En el servidor: se ejecuta cargarDatos, se espera, y el valor//   viaja serializado dentro del HTML que se envia al navegador.// - En el cliente: el resource se rehidrata de ese valor, sin re-pedir.

Compara esto con el modelo habitual de fetch-en-el-cliente: el servidor manda un HTML vacío, el navegador lo pinta, un efecto dispara la petición, y solo entonces aparecen los datos —con su parpadeo y su salto de maquetación—. El resource en SSR colapsa esa cascada: el dato ya viaja con el HTML, y el cliente continúa exactamente donde el servidor lo dejó. Menos peticiones, menos latencia percibida, y un contenido que los buscadores ven completo en la primera respuesta, sin ejecutar JavaScript.

Dos opciones afinan este comportamiento. deferStream: true le dice al servidor que espere a que el resource resuelva antes de enviar el HTML, útil cuando ese dato debe estar en la respuesta inicial por SEO o para evitar un salto de contenido. ssrLoadFrom: "initial" hace que en el servidor el resource use su initialValue en vez de ejecutar el fetcher, para datos que solo tienen sentido en el cliente.

TSX
const [critico] = createResource(cargar, { deferStream: true });// el servidor no vacia el HTML hasta que 'critico' resuelve

La elección entre esperar y transmitir es un compromiso real. Sin deferStream, el servidor envía el armazón de inmediato y va rellenando el hueco del resource cuando resuelve —el usuario ve algo antes, pero ese algo llega incompleto—. Con deferStream, el HTML inicial ya trae el dato —mejor para SEO y para evitar saltos de maquetación, a cambio de un primer byte más tardío—. No hay una respuesta universal: lo decides por dato, según si ese contenido debe estar en la respuesta inicial o puede llegar un instante después. Que la decisión sea una opción del propio resource, y no una reescritura de tu lógica de carga, es otra muestra de lo bien que el primitivo abraza el servidor.

Esta coordinación es, además, la que te ahorra los temidos desajustes de hidratación con datos. Como el cliente arranca del mismo valor que el servidor serializó, el primer árbol que construye es idéntico al que llegó en el HTML, y las discrepancias entre marcado del servidor y del cliente —esa clase de aviso que tanto cuesta rastrear— sencillamente no se producen para el estado que vino por el resource. La misma serialización que evita el doble fetch garantiza también que ambos mundos partan del mismo punto.