Skip to content

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 ​

tsx
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:

ts
const [res, handlers] = resource(fetcher, options?);

Estado ​

El primer elemento, res, es una instantánea reactiva que contiene tres señales:

CampoTipoDescripción
stateReadonlySignal<T | null>Datos resueltos, o null durante la carga o tras un error
pendingReadonlySignal<boolean>true mientras una petición está en curso
errorReadonlySignal<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í.

tsx
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:

ControladorDescripció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)
tsx
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ónTipoDescripción
initialTDatos iniciales. Mantiene pending como false en el primer renderizado (sin fallback de Suspense)
timeoutnumberCancela la operación de obtención (fetch) tras un número determinado de milisegundos y marca el recurso como fallido
keystringIdentificador único para el almacenamiento en caché y el uso compartido entre consumidores
keepPreviousStatebooleanMantiene 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:

tsx
const [user] = resource(fetchUser, { initial: cachedUser });

timeout ​

tsx
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:

tsx
const [a] = resource(fetchUser, { key: 'user' });
const [b] = resource(fetchUser, { key: 'user' }); // shares the cache entry

invalidate() 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 ​

tsx
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:

ts
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 AbortController real en cada ejecución. Este se aborta al llamar a refetch() o cancel(), 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 AbortController real para cada recurso durante la fase de preload(). 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):

ts
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:

tsx
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:

tsx
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.value es null y pending es true.
  • 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 ​

tsx
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>
    );
};

Publicado bajo la Licencia Apache 2.0