Pular para o conteúdo principal

Política de expiração do Endpoint

Por padrão, a política de cache do Reactive Data Client pode ser descrita como stale-while-revalidate. Isso significa que, quando há dados disponíveis, é possível evitar bloquear a aplicação usando os dados desatualizados. No entanto, em segundo plano, ele ainda atualizará os dados se forem antigos o suficiente.

Status de expiração​

Fresh​

Dados nesse estado são considerados novos o suficiente para que não precisem de fetch.

Stale​

Os dados ainda podem ser exibidos, porém o Reactive Data Client pode tentar revalidá-los fazendo fetch novamente.

useSuspense() considera fazer fetch na montagem, bem como quando seus parâmetros mudam. Nesses casos, ele fará o fetch se os dados forem considerados desatualizados.

React Native

Ao usar o React Navigation, eventos de foco também disparam fetches para dados desatualizados.

Invalid​

Os dados não devem ser exibidos. Qualquer componente que precise desses dados disparará o fetch e o suspense. Se nenhum componente se importar com esses dados, nenhuma ação será tomada.

Tempo de expiração​

Endpoint.dataExpiryLength​

Endpoint.dataExpiryLength define quanto tempo (em milissegundos) leva para os dados passarem do status 'fresh' para 'stale'. Experimente definir um número bem baixo, como '50', para que fiquem stale quase instantaneamente; ou um número muito grande para que permaneçam por muito tempo.

Alternar entre 'first' e 'second' muda os parâmetros. Se os dados ainda forem considerados fresh, você continuará vendo o horário antigo, sem nenhuma atualização.

Fixtures
GET /api/currentTime/:id
▶api/lastUpdated
▶getUpdated
import { lastUpdated } from './api/lastUpdated';

export const getUpdated = lastUpdated.extend({ dataExpiryLength: 10000 });
▶TimePage
import { useSuspense } from '@data-client/react';
import { getUpdated } from './getUpdated';

export default function TimePage({ id }) {
  const { updatedAt } = useSuspense(getUpdated, { id });
  return (
    <div>
      API time for {id}:{' '}
      <time>
        {updatedAt.toLocaleString('en-US', { timeStyle: 'long' })}
      </time>
    </div>
  );
}
▶Navigator
Resultado
Store▶
@data-client/rest

Longa duração de cache

LongLivingResource.ts
import {
RestEndpoint,
RestGenerics,
resource,
} from '@data-client/rest';

// We can now use LongLivingEndpoint to create endpoints that will be cached for one hour
class LongLivingEndpoint<
O extends RestGenerics,
> extends RestEndpoint<O> {
dataExpiryLength = 60 * 60 * 1000; // one hour
}

const LongLivingResource = resource({
path: '/:id',
Endpoint: LongLivingEndpoint,
});

Nunca tentar novamente em caso de erro

NoRetryResource.ts
import {
RestEndpoint,
RestGenerics,
resource,
} from '@data-client/rest';

// We can now use NoRetryEndpoint to create endpoints that will be cached for one hour
class NoRetryEndpoint<
O extends RestGenerics,
> extends RestEndpoint<O> {
errorExpiryLength = Infinity;
}

const NoRetryResource = resource({
path: '/:id',
Endpoint: NoRetryEndpoint,
});

Endpoint.invalidIfStale​

Endpoint.invalidIfStale elimina o status 'stale', fazendo com que dados que expiram sejam imediatamente considerados 'invalid'.

Isso é demonstrado pelo componente que suspende assim que seus dados ficam stale. Se os dados ainda estiverem dentro do tempo de expiração, ele simplesmente continua a exibi-los.

Fixtures
GET /api/currentTime/:id
▶api/lastUpdated
▶getUpdated
import { lastUpdated } from './api/lastUpdated';

export const getUpdated = lastUpdated.extend({
  invalidIfStale: true,
  dataExpiryLength: 5000,
});
▶TimePage
import { useSuspense } from '@data-client/react';
import { getUpdated } from './getUpdated';

export default function TimePage({ id }) {
  const { updatedAt } = useSuspense(getUpdated, { id });
  return (
    <div>
      API time for {id}:{' '}
      <time>
        {updatedAt.toLocaleString('en-US', { timeStyle: 'long' })}
      </time>
    </div>
  );
}
▶Navigator
Resultado
Store▶

Forçar atualização​

Às vezes queremos buscar dados novos, continuando a mostrar os dados antigos (stale).

Um endpoint específico​

Controller.fetch pode ser usado para disparar um fetch enquanto ainda se mostram os dados anteriores. Isso pode ser feito mesmo com dados 'fresh'.

Fixtures
GET /api/currentTime/:id
▶api/lastUpdated
▶ShowTime
import { useSuspense, useController } from '@data-client/react';
import { lastUpdated } from './api/lastUpdated';

function ShowTime() {
  const { updatedAt } = useSuspense(lastUpdated, { id: '1' });
  const ctrl = useController();
  return (
    <div>
      <time>
        {updatedAt.toLocaleString('en-US', { timeStyle: 'long' })}
      </time>{' '}
      <button onClick={() => ctrl.fetch(lastUpdated, { id: '1' })}>
        Refresh
      </button>
    </div>
  );
}
render(<ShowTime />);
Resultado
Store▶

Atualizar endpoints visíveis​

Controller.expireAll() define o status de expiração de todas as respostas que correspondem a testKey como Stale.

Fixtures
GET /api/currentTime/:id
▶api/lastUpdated
▶ShowTime
▶Loading
▶Demo
import { AsyncBoundary, useController } from '@data-client/react';

import { lastUpdated } from './api/lastUpdated';
import ShowTime from './ShowTime';
import Loading from './Loading';

function Demo() {
  const ctrl = useController();
  return (
    <div>
      <AsyncBoundary fallback={<Loading id="1" />}>
        <ShowTime id="1" />
      </AsyncBoundary>
      <AsyncBoundary fallback={<Loading id="2" />}>
        <ShowTime id="2" />
      </AsyncBoundary>
      <AsyncBoundary fallback={<Loading id="3" />}>
        <ShowTime id="3" />
      </AsyncBoundary>

      <button onClick={() => ctrl.expireAll(lastUpdated)}>
        Expire All
      </button>
      <button onClick={() => ctrl.fetch(lastUpdated, { id: '1' })}>
        Force Refresh First
      </button>
    </div>
  );
}
render(<Demo />);
Resultado
Store▶

Invalidate (suspender novamente)​

Tanto endpoints quanto entities podem ser alvos de invalidação.

Um endpoint específico​

Neste exemplo, invalidar o endpoint exibe o fallback de carregamento, já que os dados não podem ser exibidos.

Fixtures
GET /api/currentTime/:id
▶api/lastUpdated
▶ShowTime
▶Loading
▶Demo
import { AsyncBoundary, useController } from '@data-client/react';

import { lastUpdated } from './api/lastUpdated';
import ShowTime from './ShowTime';
import Loading from './Loading';

function Demo() {
  const ctrl = useController();
  return (
    <div>
      <AsyncBoundary fallback={<Loading id="1" />}>
        <ShowTime id="1" />
      </AsyncBoundary>
      <AsyncBoundary fallback={<Loading id="2" />}>
        <ShowTime id="2" />
      </AsyncBoundary>
      <AsyncBoundary fallback={<Loading id="3" />}>
        <ShowTime id="3" />
      </AsyncBoundary>

      <button onClick={() => ctrl.invalidateAll(lastUpdated)}>
        Invalidate All
      </button>
      <button
        onClick={() => ctrl.invalidate(lastUpdated, { id: '1' })}
      >
        Invalidate First
      </button>
    </div>
  );
}
render(<Demo />);
Resultado
Store▶

Qualquer endpoint com uma entity​

Usar o schema Invalidate nos permite invalidar qualquer endpoint que dependa dessa entity em sua resposta. Se o endpoint usa a entity em um Array, ela simplesmente será removida desse Array.

Fixtures
GET /api/currentTime/:id
DELETE /api/currentTime/:id
▶api/lastUpdated
▶TimePage
▶ShowTime
import { Invalidate, RestEndpoint } from '@data-client/rest';
import { AsyncBoundary, useController, useLoading } from '@data-client/react';
import { TimedEntity } from './api/lastUpdated';
import TimePage from './TimePage';

const InvalidateTimedEntity = new Invalidate(TimedEntity);
export const deleteLastUpdated = new RestEndpoint({
  path: '/api/currentTime/:id',
  method: 'DELETE',
  schema: InvalidateTimedEntity,
});

function ShowTime() {
  const ctrl = useController();
  const [handleDelete, loadingDelete] = useLoading(
    () => ctrl.fetch(deleteLastUpdated, { id: '1' }),
    [],
  );
  return (
    <div>
      <AsyncBoundary fallback={<div>loading...</div>}>
        <TimePage id="1" />
      </AsyncBoundary>
      <div>
        Current Time: <CurrentTime />
      </div>
      <button onClick={handleDelete}>
        {loadingDelete ? 'loading...' : 'Invalidate'}
      </button>
      <button
        onClick={() =>
          ctrl.setResponse(
            deleteLastUpdated,
            { id: '1' },
            { id: '1' },
          )
        }
      >
        Invalidate (without fetching DELETE)
      </button>
      <button
        onClick={() => ctrl.set([InvalidateTimedEntity], [{ id: '1' }])}
      >
        Invalidate Entity with ctrl.set
      </button>
    </div>
  );
}
render(<ShowTime />);
Resultado
Store▶

Controller.fetch() nos permite atualizar o servidor e o store. Podemos usar Controller.setResponse() ou Controller.set() quando queremos alterar diretamente o store local.

Invalidação condicional com base nos dados​

Se a invalidation deve acontecer apenas às vezes, com base nos dados da resposta, podemos retornar undefined de Entity.process.

class PriceLevel extends Entity {
price = 0;
amount = 0;

pk() {
return this.price;
}

static process(
input: [number, number],
parent: any,
key: string | undefined,
): any {
const [price, amount] = input;
if (amount === 0) return undefined;
return { price, amount };
}
}