Pular para o conteúdo principal

Endpoint

Endpoint serve para qualquer função assíncrona (uma que retorna uma Promise).

Endpoints definem uma interface padrão fortemente tipada com metadados e ciclos de vida relevantes, úteis para o Reactive Data Client e outros stores.

Pacote: @data-client/endpoint

dica

Endpoint é uma classe independente de protocolo. Experimente usar os padrões específicos de cada protocolo: REST, GraphQL ou getImage.

Interface
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 torna funções assíncronas existentes utilizáveis em qualquer contexto do Reactive Data Client, com verificação completa do 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▶

Compartilhamento de configuração​

Use Endpoint.extend() em vez de {...getTodo} (spread)

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

Ciclo de vida​

Sucesso​

Erro​

Membros do Endpoint​

Os membros também funcionam como opções (segundo argumento do construtor). Embora nenhum seja obrigatório, os primeiros têm valores padrão.

key: (params) => string​

Serializa os parâmetros. É usado para construir uma chave de busca em stores globais.

Padrão:

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

Ao sobrescrever key, não se esqueça de incluir também um testKey atualizado se você pretende usar esse método.

testKey(key): boolean​

Retorna true se a key (de fetch) fornecida corresponder a este endpoint.

Isso é usado em interceptors de mock com o <MockResolver />

name: string​

Usado em key para distinguir endpoints. Deve ser globalmente único.

O padrão é this.fetch.name

aviso

Isso pode quebrar em builds de produção que alteram nomes de funções. Isso costuma ser conhecido como function name mangling.

Nesses casos, você pode sobrescrever name ou desativar o mangling de funções.

sideEffect: boolean​

Usado para indicar que o endpoint pode ter efeitos colaterais (não idempotente). Isso o impede de ser usado com useSuspense() ou useFetch(), pois eles podem chamar o endpoint um número imprevisível de vezes.

schema: Schema​

Definição declarativa de como processar as respostas

Não informar esta opção significa que nenhuma entity será extraída.

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​

Tempo de vida personalizado, no cache, dos dados do recurso buscado. Substitui o valor definido no NetworkManager.

Saiba mais sobre o tempo de expiração

errorExpiryLength?: number​

Tempo de vida personalizado dos erros de dados do recurso buscado. Substitui o valor definido no NetworkManager.

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

'soft' usará dados desatualizados (se existirem) em caso de erro; undefined, ou não informar a opção, resultará em erro.

Saiba mais sobre errorPolicy

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

invalidIfStale: boolean​

Indica que dados desatualizados devem ser considerados inutilizáveis e, portanto, não ser retornados do cache. Isso significa que useSuspense() vai suspender quando os dados estiverem desatualizados, mesmo que já existam no cache.

pollFrequency: number​

Frequência, em milissegundos, do polling. Requer o uso de useSubscription() ou useLive() para ter efeito.

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

Quando informado, qualquer fetch com este endpoint se comportará como se o valor de retorno expectedResponse desta função fosse uma resposta de rede bem-sucedida. Quando o fetch real for concluído (com falha ou com sucesso), a atualização otimista será substituída pela resposta real da rede.

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▶
Guia de atualizações otimistas

update()​

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

Experimente usar Collections no lugar.

Elas são muito mais fáceis de usar e mais 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] };

Caso mais simples:

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

Mais atualizações:

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

O endpoint abaixo garante que o novo usuário apareça imediatamente nos usos acima.

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​

Pode ser usado para personalizar ainda mais a definição do endpoint

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


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

Além dos membros, fetch pode ser enviado para substituir a função de fetch.

Exemplos​

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

Adicionais​

Motivação​

Existe uma distinção entre

  • O que é uma API de rede
    • Como fazer uma requisição, quais campos esperar na resposta, etc.
  • Como ela é usada
    • Vincular dados, polling, disparar fetch imperativo, etc.

Por isso, há muitos benefícios em criar uma separação clara de responsabilidades entre esses dois conceitos.

Com os TypeScript Standard Endpoints, definimos um padrão para declarar em TypeScript a definição de uma API de rede.

  • Permite que autores de APIs publiquem pacotes npm contendo as interfaces de suas APIs
  • As definições podem ser consumidas por qualquer biblioteca compatível, facilitando o consumo entre bibliotecas como Vue, React e Angular
  • Escrever pipelines de geração de código fica muito mais fácil, pois a saída é mínima
  • Desenvolvedores de produto podem usar as definições em diversos contextos nos quais os comportamentos variam
  • Desenvolvedores de produto podem compartilhar código facilmente entre plataformas com necessidades de comportamento distintas, como React Native e React Web

O que há em um Endpoint​

  • Uma função que resolve os resultados
  • Uma função para armazenar esses resultados de forma única
  • Opcional: informações sobre como armazenar os dados em um cache normalizado
  • Opcional: se a requisição pode ter efeitos colaterais - para evitar chamadas repetidas