Saltar al contenido principal

Endpoint

Endpoint sirve para cualquier función asíncrona (una que devuelve una Promise).

Los Endpoints definen una interfaz estándar con tipado fuerte de metadatos y ciclos de vida relevantes útiles para Reactive Data Client y otros stores.

Paquete: @data-client/endpoint

consejo

Endpoint es una clase independiente del protocolo. Prueba mejor con los patrones específicos de cada protocolo: REST, GraphQL o getImage.

Interfaz
export interface EndpointInterface<
F extends FetchFunction = FetchFunction,
S extends Schema | undefined = Schema | undefined,
M extends true | undefined = true | undefined,
> extends EndpointExtraOptions<F> {
(...args: Parameters<F>): InferReturn<F, S>;
key(...args: Parameters<F>): string;
readonly sideEffect?: M;
readonly schema?: S;
}

Uso​

Endpoint hace que las funciones asíncronas existentes se puedan usar en cualquier contexto de Reactive Data Client con verificación completa de TypeScript.

▶interface
▶api
import { Endpoint } from '@data-client/rest';
import { Todo } from './interface';

const getTodoOriginal = (id: number): Promise<Todo> =>
  Promise.resolve({
    id,
    title: 'delectus aut autem ' + id,
    completed: false,
    userId: 1,
  });

export const getTodo = new Endpoint(getTodoOriginal);
▶React
import { useSuspense } from '@data-client/react';
import { getTodo } from './api';

function TodoDetail() {
  const todo = useSuspense(getTodo, 1);
  return <div>{todo.title}</div>;
}
render(<TodoDetail />);
Resultado
Store▶

Compartir configuración​

Usa Endpoint.extend() en lugar de {...getTodo} (spread)

const getTodoNormalized = getTodo.extend({ schema: Todo });
const getTodoUpdatingEveryFiveSeconds = getTodo.extend({ pollFrequency: 5000 });

Ciclo de vida​

Éxito​

Error​

Miembros de Endpoint​

Los miembros funcionan también como opciones (segundo argumento del constructor). Aunque ninguno es obligatorio, los primeros tienen valores por defecto.

key: (params) => string​

Serializa los parámetros. Se usa para construir una clave de búsqueda en stores globales.

Por defecto:

`${this.name} ${JSON.stringify(params)}`;
Sobrescrituras

Al sobrescribir key, asegúrate de incluir también un testKey actualizado si piensas usar ese método.

testKey(key): boolean​

Devuelve true si la key (de fetch) proporcionada coincide con este endpoint.

Se usa para los interceptors de mock con <MockResolver />

name: string​

Se usa en key para distinguir endpoints. Debe ser único a nivel global.

Por defecto es this.fetch.name

aviso

Esto puede fallar en compilaciones de producción que cambian los nombres de las funciones. Esto suele conocerse como function name mangling.

En esos casos puedes sobrescribir name o deshabilitar el mangling de funciones.

sideEffect: boolean​

Se usa para indicar que el endpoint podría tener efectos secundarios (no idempotente). Esto impide que se use con useSuspense() o useFetch(), ya que estos pueden llamar al endpoint un número impredecible de veces.

schema: Schema​

Definición declarativa de cómo procesar las respuestas

No proporcionar esta opción significa que no se extraerá ninguna entidad.

import { Endpoint, Entity } from '@data-client/endpoint';

class User extends Entity {
id = '';
username = '';
}

const getUser = new Endpoint(
({ id }) => fetch(`/users/${id}`),
{ schema: User }
);

dataExpiryLength?: number​

Tiempo de vida personalizado en la caché de los datos del recurso obtenido. Reemplazará el valor establecido en NetworkManager.

Más información sobre el tiempo de caducidad

errorExpiryLength?: number​

Tiempo de vida personalizado de los errores del recurso obtenido. Reemplazará el valor establecido en NetworkManager.

errorPolicy?: (error: any) => 'soft' | undefined​

'soft' usará los datos obsoletos (si existen) en caso de error; undefined o no proporcionar la opción provocará un error.

Más información sobre errorPolicy

errorPolicy(error) {
return error.status >= 500 ? 'soft' : undefined;
}

invalidIfStale: boolean​

Indica que los datos obsoletos deben considerarse inutilizables y, por tanto, no devolverse desde la caché. Esto significa que useSuspense() se suspenderá cuando los datos estén obsoletos aunque ya existan en la caché.

pollFrequency: number​

Frecuencia en milisegundos con la que se realiza el sondeo. Requiere usar useSubscription() o useLive() para tener efecto.

getOptimisticResponse: (snap, ...args) => expectedResponse​

Cuando se proporciona, cualquier fetch con este endpoint se comportará como si el valor de retorno expectedResponse de esta función fuera una respuesta de red exitosa. Cuando el fetch real se completa (ya sea con fallo o con éxito), la actualización optimista se reemplaza por la respuesta de red real.

import { resource } from '@data-client/rest';
import { Post } from './Post';

export { Post };

export const PostResource = resource({
  path: '/posts/:id',
  searchParams: {} as { userId?: string | number } | undefined,
  schema: Post,
}).extend('vote', {
  path: '/posts/:id/vote',
  method: 'POST',
  body: undefined,
  schema: Post,
  getOptimisticResponse(snapshot, { id }) {
    const post = snapshot.get(Post, { id });
    if (!post) throw snapshot.abort;
    return {
      id,
      votes: post.votes + 1,
    };
  },
});
Resultado
Store▶
Guía de actualizaciones optimistas

update()​

(normalizedResponseOfThis, ...args) =>
({ [endpointKey]: (normalizedResponseOfEndpointToUpdate) => updatedNormalizedResponse) })
consejo

Prueba a usar Collections en su lugar.

¡Son mucho más fáciles de usar y más robustas!

UpdateType.ts
type UpdateFunction<
Source extends EndpointInterface,
Updaters extends Record<string, any> = Record<string, any>,
> = (
source: ResultEntry<Source>,
...args: Parameters<Source>
) => { [K in keyof Updaters]: (result: Updaters[K]) => Updaters[K] };

El caso más sencillo:

userEndpoint.ts
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId: string) => ({
[userList.key()]: (users = []) => [newUserId, ...users],
}),
});

Más actualizaciones:

Component.tsx
const allusers = useSuspense(userList);
const adminUsers = useSuspense(userList, { admin: true });

El endpoint siguiente garantiza que el nuevo usuario aparezca de inmediato en los usos anteriores.

userEndpoint.ts
const createUser = new RestEndpoint({
path: '/user',
method: 'POST',
schema: User,
update: (newUserId, newUser) => {
const updates = {
[userList.key()]: (users = []) => [newUserId, ...users],
];
if (newUser.isAdmin) {
updates[userList.key({ admin: true })] = (users = []) => [newUserId, ...users];
}
return updates;
},
});

extend(options): Endpoint​

Se puede usar para personalizar aún más la definición del endpoint

const getUser = new Endpoint(({ id }) => fetch(`/users/${id}`));


const getUserNormalized = getUser.extend({ schema: User });

Además de los miembros, se puede enviar fetch para sobrescribir la función fetch.

Ejemplos​

import { Endpoint } from '@data-client/endpoint';

const UserDetail = new Endpoint(
({ id }) => fetch(`/users/${id}`).then(res => res.json())
);
import { useSuspense, useController } from '@data-client/react';
import { UserDetail } from './api/User';
import UserForm from './UserForm';

function UserProfile({ id }: { id: string }) {
const user = useSuspense(UserDetail, { id });
const ctrl = useController();

return <UserForm user={user} onSubmit={() => ctrl.fetch(UserDetail)} />;
}

Adicional​

Motivación​

Hay una distinción entre

  • Qué es una API de red
    • Cómo hacer una petición, qué campos se esperan en la respuesta, etc.
  • Cómo se usa
    • Enlazar datos, sondeo (polling), disparar un fetch imperativo, etc.

Por lo tanto, separar claramente las responsabilidades de estos dos conceptos tiene muchos beneficios.

Con los TypeScript Standard Endpoints definimos un estándar para declarar en TypeScript la definición de una API de red.

  • Permite a los autores de APIs publicar paquetes npm con las interfaces de su API
  • Las definiciones las puede consumir cualquier librería compatible, lo que facilita su uso en librerías como Vue, React o Angular
  • Escribir pipelines de generación de código se vuelve mucho más fácil, ya que la salida es mínima
  • Los desarrolladores de producto pueden usar las definiciones en multitud de contextos donde los comportamientos varían
  • Los desarrolladores de producto pueden compartir código fácilmente entre plataformas con necesidades de comportamiento distintas, como React Native y React Web

Qué hay en un Endpoint​

  • Una función que resuelve los resultados
  • Una función para almacenar esos resultados de forma única
  • Opcional: información sobre cómo almacenar los datos en una caché normalizada
  • Opcional: si la petición podría tener efectos secundarios, para evitar llamadas repetidas