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.
Cerramos el nivel con las dos caras que convierten a createResource en la pieza central de la carga de datos de todo SolidStart: su tipado y su relación con el servidor. El tipado no es un trámite —el valor de un resource es honestamente T | undefined, y ese undefined es una verdad del dominio que TypeScript te ayuda a no ignorar—. Y la afinidad con el SSR no es casualidad ni añadido posterior: el resource fue diseñado desde el primer día para que la misma carga que escribes para el cliente funcione en el servidor, viaje serializada en el HTML y se rehidrate sin volver a pedir. Entender ambas cosas es entender por qué este primitivo, y no otro, sostiene el fullstack de Solid.
- Tipar un resource con sus genéricos
TyS, y entender por qué el valor esT | undefined. - Usar
Resource<T>para firmar funciones que reciben un resource ya creado. - Explicar por qué
createResourcees amistoso con el renderizado en servidor. - Conocer las opciones de SSR —
deferStream,ssrLoadFrom— y la carga en el servidor sin doble fetch.
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.
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 fuente
const [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.
<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.
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.
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.
// 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.
flowchart LR SV[servidor ejecuta el fetcher] --> RS[resource resuelto] RS --> HT[valor serializado en el HTML] HT --> CL[cliente rehidrata sin re-pedir] CL --> UI[primer render coincide con el servidor] style RS fill:#a6e3a1,color:#11111b style HT fill:#89b4fa,color:#11111b style UI fill:#a6e3a1,color:#11111b
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.
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.
T | undefined honesto
El valor es T o undefined porque antes de resolver no hay dato. Se estrecha con Show o Suspense; initialValue lo elimina del tipo.
El tipo Resource de T
Describe el accessor completo, valor mas loading, error, latest y state. Firma componentes que consumen un resource que no crearon.
Serializa e hidrata
El servidor resuelve, serializa el valor en el HTML y el cliente lo rehidrata sin volver a pedir. Una carga, no dos.
En SSR, la primera ejecución del fetcher ocurre en el servidor, donde no existe window, ni localStorage, ni document. Un fetcher que toque esas APIs sin protección reventará el render del servidor. Usa isServer de solid-js/web para ramificar la lógica que solo tiene sentido en el navegador, o mueve ese trabajo a un onMount, que nunca corre en el servidor. La regla general: el fetcher debe ser código isomórfico —fetch a una URL absoluta, lectura de datos— y dejar cualquier dependencia del navegador fuera de su camino síncrono. Sobre este mismo diseño, SolidStart construye query y createAsync, que añaden deduplicación y caché por petición; pero el cimiento que los hace posibles es esta afinidad de createResource con el servidor.
Mira hacia atrás el nivel entero y verás que un único primitivo ha sostenido una superficie enorme: cargar datos con dos firmas, exponer un ciclo de vida de cinco estados, recargar bajo demanda, escribir optimista, resolver condiciones de carrera, cancelar peticiones, y ahora tiparse con honestidad y renderizarse en el servidor. Que todo eso quepa en un solo primitivo no es una casualidad de diseño: es la consecuencia directa de la decisión fundacional que abrió el nivel —modelar lo asíncrono como un valor reactivo—. Porque una vez que «el dato que llegará» es un signal, cada capacidad que necesitas para gobernarlo resulta ser una capacidad que la reactividad ya tenía. Los estados son el ciclo de vida de ese signal. refetch es su recálculo y mutate su escritura. El descarte de peticiones obsoletas es el compromiso del signal con su fuente en vez de con su respuesta. Y la afinidad con el SSR es la más reveladora de todas, porque expone por qué esta unificación no es un truco de conveniencia sino una ventaja arquitectónica real. El renderizado en servidor es, en esencia, un problema de asincronía a gran escala: el servidor debe esperar datos, decidir qué enviar y cuándo, y entregarle al cliente un estado que este pueda continuar sin rehacer el trabajo. Un framework que trata la carga de datos como una capa aparte de su reactividad tiene que construir un puente frágil entre ambos mundos —hidratar estado, evitar dobles peticiones, sincronizar el momento del render con el de los datos— y esos puentes son donde viven los bugs de SSR más tercos. Solid no necesita ese puente porque nunca separó las orillas: como el resource es un signal, serializar su valor y rehidratarlo es serializar y rehidratar reactividad, algo que el sistema ya sabe hacer. La misma abstracción que te dio la carga en el cliente te da el SSR sin una segunda mentalidad. Esta es la lección estratégica que te llevas del nivel 26 y que ilumina todo lo que viene —Suspense, transitions, ErrorBoundary, y la carga de datos de SolidStart—: no aprendiste una API para pedir cosas por la red, aprendiste que en Solid lo asíncrono nunca fue un dominio extranjero, sino reactividad con una fuente que a veces está lejos y a veces está en otro ordenador. Cuando esa idea se te asienta, la carga de datos deja de ser lo difícil del frontend y pasa a ser, simplemente, más de lo que ya sabías hacer.
- Crea un resource genérico explícito con
createResource<Usuario, number>y comprueba en el editor quedata()esUsuario | undefined. - Estrecha ese
undefinedcon unShowy confirma que dentro del hijo el valor ya no lo incluye; luego elimínalo desde el origen coninitialValue. - Escribe un componente que reciba un
Resource<T>por props y renderice su ciclo de vida sin crear el resource él mismo. - Monta el resource en una página de SolidStart bajo
Suspensey verifica en la pestaña de red que el cliente no vuelve a pedir el dato que el servidor ya serializó. - Añade
deferStream: truea un resource crítico y observa que el HTML inicial ya trae su contenido; luego mete una lectura dewindowen el fetcher y comprueba cómoisServerevita el fallo en el servidor.