Recursos
resource es la primitiva de datos asíncronos de PreffX. Envuelve una función de obtención de datos (fetcher —una función asíncrona—) y expone un estado reactivo (state, error, pending), además de un conjunto de controladores (refetch, mutate, cancel, invalidate) para gestionar el ciclo de vida de la obtención de datos.
Los recursos son isomórficos: el mismo código de componente se ejecuta tanto en el servidor (precarga mediante SSR) como en el cliente (hidratación + reactividad), sin necesidad de bifurcaciones en el código.
Uso básico
import type { PC } from 'preffx';
export const UserCard: PC = (_props, { resource, computed }) => {
const [user] = resource(async () => {
const res = await fetch('/api/user');
return res.json();
});
const display = computed(() => user.state.value?.name ?? 'loading');
return (
<article className="user">
<h1>{display}</h1>
</article>
);
};resource(fetcher) devuelve una tupla:
const [res, handlers] = resource(fetcher, options?);Estado
El primer elemento, res, es una instantánea reactiva que contiene tres señales:
| Campo | Tipo | Descripción |
|---|---|---|
state | ReadonlySignal<T | null> | Datos resueltos, o null durante la carga o tras un error |
pending | ReadonlySignal<boolean> | true mientras una petición está en curso |
error | ReadonlySignal<E | null> | Último error, o null |
state y error están separados deliberadamente: un error no forma parte del estado de los datos. Esto mantiene ambos aspectos independientes entre sí.
const [res] = resource<string, ApiError>(fetchUser);
const view = computed(() => {
if (res.pending.value) return 'loading…';
if (res.error.value) return `Error: ${res.error.value.message}`;
return res.state.value;
});El tipo de error E se infiere a partir del tipo de rechazo del fetcher, o bien puede indicarse de forma explícita: resource<string, ApiError>(fetcher).
Controladores
El segundo elemento es un objeto de control:
| Controlador | Descripción |
|---|---|
refetch() | Vuelve a ejecutar el fetcher (cancelando primero cualquier petición en curso) |
mutate(v) | Establece el valor resuelto de forma optimista, sin necesidad de volver a realizar la petición |
cancel() | Cancela la petición en curso y restablece el estado pending. No afecta a la caché |
invalidate() | Invalida la entrada en caché correspondiente a la key de este recurso (solo para claves explícitas) |
const [user, { refetch, mutate, cancel }] = resource(fetchUser);
return (
<div>
<button onClick={refetch}>Refresh</button>
<button onClick={() => mutate({ name: 'Optimistic' })}>Set</button>
<button onClick={cancel}>Cancel</button>
</div>
);Opciones
resource(fetcher, options?) acepta:
| Opción | Tipo | Descripción |
|---|---|---|
initial | T | Datos iniciales. Mantiene pending como false en el primer renderizado (sin fallback de Suspense) |
timeout | number | Cancela la operación de obtención (fetch) tras un número determinado de milisegundos y marca el recurso como fallido |
key | string | Identificador único para el almacenamiento en caché y el uso compartido entre consumidores |
keepPreviousState | boolean | Mantiene el state anterior mientras se realiza una nueva obtención (refetch) o se reejecuta debido a un cambio en las dependencias |
initial
Cuando se proporciona initial, los datos ya están disponibles; por lo tanto, pending comienza como false y Suspense no muestra ningún fallback:
const [user] = resource(fetchUser, { initial: cachedUser });timeout
const [user] = resource(fetchUser, { timeout: 5000 });En caso de tiempo de espera (timeout), la solicitud en curso se interrumpe y error se establece como un error de tipo «tiempo de espera agotado» (Resource timed out).
key y almacenamiento en caché
Las claves explícitas permiten un almacenamiento en caché a nivel de raíz, compartido entre los consumidores. Dos componentes que utilizan la misma key comparten una única entrada de state/error y solo realizan la obtención de datos una vez:
const [a] = resource(fetchUser, { key: 'user' });
const [b] = resource(fetchUser, { key: 'user' }); // shares the cache entryinvalidate() elimina esa entrada, de modo que el siguiente consumidor vuelva a realizar la obtención. Las claves posicionales (sin key) no intervienen en el almacenamiento en caché.
keepPreviousState
const [user, { refetch }] = resource(fetchUser, { keepPreviousState: true });Mientras una nueva obtención de datos está en curso, el estado anterior permanece visible (estrategia stale-while-revalidate) y solo se actualizan los valores de error o pending.
Señal de interrupción (Abort signal)
La función de obtención de datos (fetcher) recibe siempre un AbortSignal como primer argumento:
resource(async (signal) => {
const res = await fetch('/api/data', { signal });
return res.json();
});Esto funciona de manera coherente tanto en el cliente como en el servidor:
- Cliente: se crea un
AbortControllerreal en cada ejecución. Este se aborta al llamar arefetch()ocancel(), al producirse un tiempo de espera (timeout) o al realizarse un desmontaje (unmount). Cuando la señal se aborta, no se aplican ni los datos ni el error. - Servidor (SSR): se crea un
AbortControllerreal para cada recurso durante la fase depreload(). Si la señal se aborta, no se aplican ni los datos ni el error, lo cual coincide con el comportamiento en el cliente.
Nota: abortar la señal cancela la solicitud; no da lugar a un error. Para indicar un fallo (por ejemplo, «falta de permisos»), lanza un error desde la función de obtención de datos (fetcher):
resource(async () => {
if (!hasPermission()) throw new ForbiddenError('no access');
return fetchData();
});Recarga basada en dependencias
Si la función de obtención (fetcher) lee una señal dentro de su cuerpo, el recurso se vuelve a ejecutar automáticamente cuando dicha señal cambia:
const userId = signal(1);
const [user] = resource(async () => {
return fetchUser(userId.value); // re-runs when userId changes
});Renderizado en el servidor (SSR)
En el servidor, resource no ejecuta un efecto; en su lugar, registra su función de obtención de datos (fetcher) para preload(). El mismo componente funciona sin necesidad de cambios:
import { createRoot } from 'preffx/server';
const server = createRoot();
const { preload, serialize } = server.renderToString(App, {});
await preload(); // resolve every registered resource
const html = serialize(); // markup + preload data script- Antes de
preload(),state.valueesnullypendingestrue. - Después de
preload(), el valor resuelto se incorpora al marcado y se escribe en el script de datos de precarga. - En el cliente, durante la hidratación, el recurso lee el valor precargado y omite la operación de obtención (fetch).
Ejemplo completo
import type { PC } from 'preffx';
type User = { id: number; name: string };
const UserCard: PC = (_props, { resource, computed }) => {
const [user, { refetch }] = resource<User>(
async (signal) => {
const res = await fetch('/api/user', { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
{ timeout: 5000 }
);
const view = computed(() => {
if (user.pending.value) return 'Loading…';
if (user.error.value) return `Error: ${user.error.value.message}`;
return user.state.value?.name;
});
return (
<div>
<p>{view}</p>
<button onClick={refetch}>Refresh</button>
</div>
);
};