跳到主要内容

Endpoint 过期策略

默认情况下,Reactive Data Client 的缓存策略可以描述为 stale-while-revalidate。这意味着当数据可用时,它可以使用过时的数据,从而避免阻塞应用。不过在后台,如果数据足够旧,它仍然会刷新数据。

过期状态​

新鲜(Fresh)​

处于这种状态的数据被认为足够新,无需获取。

过时(Stale)​

数据仍然可以展示,但 Reactive Data Client 可能会尝试重新获取以重新验证。

useSuspense() 会在挂载时以及参数变化时考虑是否获取。在这些情况下,如果数据被视为过时,它就会发起获取。

React Native

使用 React Navigation 时,焦点事件也会为过时的数据触发获取。

无效(Invalid)​

数据不应展示。任何需要这些数据的组件都会触发获取并挂起。如果没有组件关心这些数据,则不会执行任何操作。

过期时间​

Endpoint.dataExpiryLength​

Endpoint.dataExpiryLength 用于设置数据从“新鲜”状态转变为“过时”状态所需的时间(毫秒)。试着把它设置为很小的数字,比如 '50',让数据几乎立即变为过时;或者设置为很大的数字,让数据保留很长时间。

在 'first' 和 'second' 之间切换会改变参数。如果数据仍被视为新鲜,你会一直看到旧的时间,而不会发生任何刷新。

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
结果
Store▶
@data-client/rest

较长的缓存寿命

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

出错时从不重试

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 去掉了“过时”状态,使得过期的数据立即被视为“无效”。

在演示中,组件的数据一旦过时,组件就会挂起。如果数据仍在过期时间之内,组件就会继续展示它。

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
结果
Store▶

强制刷新​

有时我们想要获取新数据,同时继续展示旧的(过时的)数据。

某个特定的 endpoint​

Controller.fetch 可以用来触发获取,同时仍展示之前的数据。即使数据是“新鲜”的,也可以这样做。

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 />);
结果
Store▶

刷新可见的 endpoint​

Controller.expireAll() 会把所有与 testKey 匹配的响应的过期状态设置为过时。

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 />);
结果
Store▶

使失效(重新挂起)​

endpoint 和 Entity 都可以作为失效的目标。

某个特定的 endpoint​

在这个示例中,使 endpoint 失效会显示加载 fallback,因为此时数据不允许展示。

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 />);
结果
Store▶

包含某个 Entity 的任意 endpoint​

使用 Invalidate schema,我们可以让响应中依赖该 Entity 的_任意_ endpoint 失效。如果 endpoint 在 Array 中使用了该 Entity,它只会被从该 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 />);
结果
Store▶

Controller.fetch() 让我们可以同时更新服务器和 store。当我们想直接修改本地 store 时,可以使用 Controller.setResponse() 或 Controller.set()。

根据数据有条件地失效​

如果 invalidation 只应在某些情况下根据响应数据发生,我们可以从 Entity.process 返回 undefined。

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