Skip to content

Commit

Permalink
docs: Add expiry policy doc page (#1344)
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Oct 5, 2021
1 parent 7feffd4 commit 79c1a23
Show file tree
Hide file tree
Showing 17 changed files with 707 additions and 37 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -86,7 +86,7 @@ For the small price of 8kb gziped.    [🏁Get started now](https://re
- [x] ♻️ Optional [redux integration](https://resthooks.io/docs/guides/redux)
- [x] 📙 [Storybook mocking](https://resthooks.io/docs/guides/storybook)
- [x] 📱 [React Native](https://facebook.github.io/react-native/) support
- [x] 🚯 Declarative cache lifetime policy
- [x] 🚯 [Declarative cache lifetime policy](https://resthooks.io/docs/getting-started/expiry-policy)

## Principals of Rest Hooks

Expand Down
4 changes: 4 additions & 0 deletions docs/api/Endpoint.md
Expand Up @@ -147,6 +147,8 @@ In addition to the members, `fetch` can be sent to override the fetch function.

Custom data cache lifetime for the fetched resource. Will override the value set in NetworkManager.

[Learn more about expiry time](../getting-started/expiry-policy#expiry-time)

#### errorExpiryLength?: number {#errorexpirylength}

Custom data error lifetime for the fetched resource. Will override the value set in NetworkManager.
Expand All @@ -156,6 +158,8 @@ Custom data error lifetime for the fetched resource. Will override the value set
'soft' will use stale data (if exists) in case of error; undefined or not providing option will result
in error.

[Learn more about errorPolicy](../getting-started/expiry-policy#error-policy)

#### invalidIfStale: boolean {#invalidifstale}

Indicates stale data should be considered unusable and thus not be returned from the cache. This means
Expand Down
2 changes: 1 addition & 1 deletion docs/api/useResource.md
Expand Up @@ -34,7 +34,7 @@ Excellent for retrieving the data you need.

`useResource()` [suspends](../getting-started/data-dependency#async-fallbacks-loadingerror) rendering until the data is available. This is much like [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await)ing an [async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) function. That is to say, the lines after the function won't be run until resolution (data is available).

Cache policy is [Stale-While-Revalidate](https://tools.ietf.org/html/rfc5861) by default but also [configurable](https://resthooks.io/docs/guides/resource-lifetime).
Cache policy is [Stale-While-Revalidate](https://tools.ietf.org/html/rfc5861) by default but also [configurable](../getting-started/expiry-policy.md).

- Triggers fetch:
- On first-render
Expand Down
235 changes: 227 additions & 8 deletions docs/getting-started/expiry-policy.md
Expand Up @@ -3,33 +3,252 @@ title: Expiry Policy
sidebar_label: Expiry Policy
---

Stale-while-revalidate
import HooksPlayground from '@site/src/components/HooksPlayground';

## TTL
By default, Rest Hooks cache policy can be described as [stale-while-revalidate](https://web.dev/stale-while-revalidate/).
This means that when data is available it can avoid blocking the application by using the stale data. However, in the background
it will still refresh the data if old enough.

To explain these concepts we'll be faking an endpoint that gives us the current time so it is easy to tell how stale it is.

```tsx title="lastUpdated.ts"
const mockFetch = ({ id, delay = 150 }) =>
new Promise(resolve =>
setTimeout(
() =>
resolve({
id,
updatedAt: new Date().toISOString(),
}),
delay,
),
);
class TimedEntity extends Entity {
pk() {
return this.id;
}
static schema = {
updatedAt: Date,
};
}

const lastUpdated = new Endpoint(mockFetch, { schema: TimedEntity });
```

## Expiry status

### Fresh

Data in this state is considered new enough that it doesn't need to fetch.

### Stale

Data is still allowed to be shown, however Rest Hooks might attempt to revalidate by fetching again.

[useResource()](../api/useResource.md) considers fetching on mount as well as when its parameters change.
In these cases it will fetch if the data is considered stale.

### Invalid

Data should not be shown. Any components needing this data will trigger fetch and suspense. If
no components care about this data no action will be taken.

## Expiry Time

[Endpoint.dataExpiryTime](../api/Endpoint#dataexpirylength) sets how long (in miliseconds) it takes for data
to transition from 'fresh' to 'stale' status. Try setting it to a very low number like '50'
to make it becomes stale almost instantly; or a very large number to stay around for a long time.

Toggling between 'first' and 'second' changes the parameters. If the data is still considered fresh
you will continue to see the old time without any refresh.

<HooksPlayground>

```tsx
const lastUpdated = lastUpdated.extend({ dataExpiryLength: 10000 });

function TimePage({ id }) {
const { updatedAt } = useResource(lastUpdated, { id });
return (
<div>
API Time:{' '}
<time>
{Intl.DateTimeFormat('en-US', { timeStyle: 'long' }).format(updatedAt)}
</time>
</div>
);
}

function Navigator() {
const [id, setId] = React.useState('1');
const handleChange = e => setId(e.currentTarget.value);
return (
<div>
<div>
<button value="1" onClick={handleChange}>
First
</button>
<button value="2" onClick={handleChange}>
Second
</button>
</div>
<TimePage id={id} />
<div>
Current Time: <CurrentTime />
</div>
</div>
);
}
render(<Navigator />);
```

</HooksPlayground>

## Force refresh

// fetch
[Controller.fetch](../api/Controller#fetch) can be used to trigger a fetch while still showing
the previous data. This can be done even with 'fresh' data.

<HooksPlayground>

```tsx
function ShowTime() {
const { updatedAt } = useResource(lastUpdated, { id: '1' });
const { fetch } = useController();
return (
<div>
<time>
{Intl.DateTimeFormat('en-US', { timeStyle: 'long' }).format(updatedAt)}
</time>{' '}
<button onClick={() => fetch(lastUpdated, { id: '1' })}>Refresh</button>
</div>
);
}
render(<ShowTime />);
```

</HooksPlayground>

## Invalidate (re-suspend)

Both endpoints and entities can be targetted to be invalidated.

### A specific endpoint

// invalidate
In this example we can see invalidating the endpoint shows the loading fallback since the data is not allowed to be displayed.

<HooksPlayground>

```tsx
function ShowTime() {
const { updatedAt } = useResource(lastUpdated, { id: '1' });
const { invalidate } = useController();
return (
<div>
<time>
{Intl.DateTimeFormat('en-US', { timeStyle: 'long' }).format(updatedAt)}
</time>{' '}
<button onClick={() => invalidate(lastUpdated, { id: '1' })}>
Invalidate
</button>
</div>
);
}
render(<ShowTime />);
```

</HooksPlayground>

### Any endpoint with an entity

// deletes
Using [Delete](../api/Delete.md) allows us to invalidate _any_ endpoint that includes that relies on that entity in their
response. If the endpoint uses the entity in an Array, it will simply be removed from that Array.

<HooksPlayground>

```tsx
const mockDelete = ({ id }) => Promise.resolve({ id });
const deleteLastUpdated = new Endpoint(mockDelete, {
schema: new schema.Delete(TimedEntity),
});

function ShowTime() {
const { updatedAt } = useResource(lastUpdated, { id: '1' });
const { fetch } = useController();
return (
<div>
<time>
{Intl.DateTimeFormat('en-US', { timeStyle: 'long' }).format(updatedAt)}
</time>{' '}
<button onClick={() => fetch(deleteLastUpdated, { id: '1' })}>
Delete
</button>
</div>
);
}
render(<ShowTime />);
```

</HooksPlayground>

## Error policy

[errorPolicy](../api/Endpoint#errorpolicy)
[Endpoint.errorPolicy](../api/Endpoint#errorpolicy) controls cache behavior upon a fetch rejection.
It uses the rejection error to determine whether it should be treated as 'soft' or 'hard' error.

### Soft

Soft errors will not invalidate a response if it is already available. However, if there is currently
no data available, it will mark that endpoint as rejected, causing [useResource()](../api/useResource.md) to throw an
error. This can be caught with [NetworkErrorBoundary](../api/NetworkErrorBoundary.md)

### Hard

### Example
Hard errors always invalidate a response with the rejection - even when data has previously made available.

<HooksPlayground>

```tsx
const lastUpdated = lastUpdated.extend({
errorPolicy: error =>
error.status >= 500 ? ('hard' as const) : ('soft' as const),
// we need this to ignore 'status' sent in arguments
key({ id }) {
return `lastUpdated ${id}`;
},
});
const mockError = ({ status }) => {
const error = new Error('fake error');
error.status = status;
return Promise.reject(error);
};
const alwaysError = lastUpdated.extend({ fetch: mockError });

function ShowTime() {
const { updatedAt } = useResource(lastUpdated, { id: '1' });
const { fetch } = useController();
return (
<div>
<time>
{Intl.DateTimeFormat('en-US', { timeStyle: 'long' }).format(updatedAt)}
</time>{' '}
<div>
<button onClick={() => fetch(alwaysError, { id: '1', status: 400 })}>
Soft Error
</button>
<button onClick={() => fetch(alwaysError, { id: '1', status: 500 })}>
Hard Error
</button>
</div>
</div>
);
}
render(<ShowTime />);
```

</HooksPlayground>

### Policy for Resources

Since `500`s indicate a failure of the server, we want to use stale data
if it exists. On the other hand, something like a `400` indicates 'user error', which
Expand All @@ -46,4 +265,4 @@ Since this is the typical behavior for REST APIs, this is the default policy in
error.status >= 500 ? ('soft' as const) : undefined,
};
}
```
```
2 changes: 1 addition & 1 deletion packages/core/README.md
Expand Up @@ -83,7 +83,7 @@ For the small price of 7kb gziped. &nbsp;&nbsp; [🏁Get started now](https://re
- [x] ♻️ Optional [redux integration](https://resthooks.io/docs/guides/redux)
- [x] 📙 [Storybook mocking](https://resthooks.io/docs/guides/storybook)
- [x] 📱 [React Native](https://facebook.github.io/react-native/) support
- [x] 🚯 Declarative cache lifetime policy
- [x] 🚯 [Declarative cache lifetime policy](https://resthooks.io/docs/getting-started/expiry-policy)

## Principals of Rest Hooks

Expand Down
2 changes: 1 addition & 1 deletion packages/rest-hooks/README.md
Expand Up @@ -82,7 +82,7 @@ For the small price of 8kb gziped. &nbsp;&nbsp; [🏁Get started now](https://re
- [x] ♻️ Optional [redux integration](https://resthooks.io/docs/guides/redux)
- [x] 📙 [Storybook mocking](https://resthooks.io/docs/guides/storybook)
- [x] 📱 [React Native](https://facebook.github.io/react-native/) support
- [x] 🚯 Declarative cache lifetime policy
- [x] 🚯 [Declarative cache lifetime policy](https://resthooks.io/docs/getting-started/expiry-policy)

## Principals of Rest Hooks

Expand Down
6 changes: 6 additions & 0 deletions website/docusaurus.config.js
Expand Up @@ -35,6 +35,12 @@ module.exports = {
href: 'https://fonts.googleapis.com',
crossOrigin: true,
},
{
rel: 'preload',
href: 'https://fonts.googleapis.com/css2?family=Rubik:wght@300..900&family=Rubik:ital,wght@1,300..900&family=Roboto+Mono:wght@100..700&family=Roboto+Mono:ital,wght@1,100..700',
as: 'style',
crossOrigin: true,
},
],
favicon: 'img/favicon/favicon.ico',
themes: ['@docusaurus/theme-live-codeblock'],
Expand Down
4 changes: 4 additions & 0 deletions website/sidebars.json
Expand Up @@ -24,6 +24,10 @@
{
"type": "doc",
"id": "getting-started/entity"
},
{
"type": "doc",
"id": "getting-started/expiry-policy"
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/Demo/EndpointDemo.js
Expand Up @@ -20,7 +20,7 @@ render(<TodoDetail/>);
`;

const EndpointDemo = props => (
<Playground scope={scope} noInline>
<Playground scope={scope} noInline groupId="homepage-demo">
{code}
</Playground>
);
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/Demo/GraphQLDemo.js
Expand Up @@ -28,7 +28,7 @@ render(<UserDetail/>);
`;

const GraphQLDemo = props => (
<Playground scope={scope} noInline>
<Playground scope={scope} noInline groupId="homepage-demo">
{code}
</Playground>
);
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/Demo/ResourceDemo.js
Expand Up @@ -22,7 +22,7 @@ render(<TodoDetail/>);
`;

const ResourceDemo = props => (
<Playground scope={scope} noInline>
<Playground scope={scope} noInline groupId="homepage-demo">
{code}
</Playground>
);
Expand Down

0 comments on commit 79c1a23

Please sign in to comment.