Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cool-dryers-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@data-client/react': patch
---

Update README examples to have more options configured
18 changes: 18 additions & 0 deletions .changeset/many-pears-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@data-client/react': minor
'@data-client/core': minor
'@rest-hooks/core': minor
'@rest-hooks/react': minor
---

Add controller.expireAll() that sets all responses to *STALE*

```ts
controller.expireAll(ArticleResource.getList);
```

This is like controller.invalidateAll(); but will continue showing
stale data while it is refetched.

This is sometimes useful to trigger refresh of only data presently shown
when there are many parameterizations in cache.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,16 @@ class Article extends Entity {
const UserResource = createResource({
path: '/users/:id',
schema: User,
optimistic: true,
});

const ArticleResource = createResource({
path: '/articles/:id',
schema: Article,
searchParams: {} as
| { beginAt?: string; endAt?: string; author?: string }
| undefined,
optimistic: true,
});
```

Expand All @@ -86,12 +91,12 @@ const ArticleResource = createResource({
```tsx
const article = useSuspense(ArticleResource.get, { id });
return (
<>
<article>
<h2>
{article.title} by {article.author.username}
</h2>
<p>{article.body}</p>
</>
</article>
);
```

Expand Down
58 changes: 52 additions & 6 deletions docs/core/api/Controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import TabItem from '@theme/TabItem';
class Controller {
/*************** Action Dispatchers ***************/
fetch(endpoint, ...args): ReturnType<E>;
expireAll({ testKey }): Promise<void>;
invalidate(endpoint, ...args): Promise<void>;
invalidateAll({ testKey }): Promise<void>;
resetEntireStore(): Promise<void>;
Expand All @@ -24,7 +25,7 @@ class Controller {
subscribe(endpoint, ...args): Promise<void>;
unsubscribe(endpoint, ...args): Promise<void>;
/*************** Data Access ***************/
getResponse(endpoint, ...args, state): { data, expiryStatus, expiresAt };
getResponse(endpoint, ...args, state): { data; expiryStatus; expiresAt };
getError(endpoint, ...args, state): ErrorTypes | undefined;
snapshot(state: State<unknown>, fetchedAt?: number): SnapshotInterface;
getState(): State<unknown>;
Expand Down Expand Up @@ -56,7 +57,9 @@ function CreatePost() {

return (
<form
onSubmit={e => ctrl.fetch(PostResource.getList.push, new FormData(e.target))}
onSubmit={e =>
ctrl.fetch(PostResource.getList.push, new FormData(e.target))
}
>
{/* ... */}
</form>
Expand Down Expand Up @@ -141,6 +144,44 @@ post.pk();
- Identical requests are deduplicated globally; allowing only one inflight request at a time.
- To ensure a _new_ request is started, make sure to abort any existing inflight requests.

## expireAll({ testKey }) {#expireAll}

Sets all responses' [expiry status](../concepts/expiry-policy.md) matching `testKey` to [Stale](../concepts/expiry-policy.md#stale).

This is sometimes useful to trigger refresh of only data presently shown
when there are many parameterizations in cache.

```tsx
import { type Controller, useController } from '@data-client/react';

const createTradeHandler = (ctrl: Controller) => async trade => {
await ctrl.fetch(TradeResource.getList.push({ user: user.id }, trade));
// highlight-start
ctrl.expireAll(AccountResource.get);
ctrl.expireAll(AccountResource.getList);
// highlight-end
};

function CreateTrade({ id }: { id: string }) {
const handleTrade = createTradeHandler(useController());

return (
<Form onSubmit={handleTrade}>
<FormField name="ticker" />
<FormField name="amount" type="number" />
<FormField name="price" type="number" />
</Form>
);
}
```

:::tip

To reduce load, improve performance, and improve state consistency; it can often be
better to [include mutation sideeffects in the mutation response](/rest/guides/rpc).

:::

## invalidate(endpoint, ...args) {#invalidate}

Forces refetching and suspense on [useSuspense](./useSuspense.md) with the same Endpoint
Expand All @@ -162,7 +203,7 @@ function ArticleName({ id }: { id: string }) {

:::tip

To refresh while continuing to display stale data - [Controller.fetch](#fetch) instead.
To refresh while continuing to display stale data - [Controller.fetch](#fetch).

:::

Expand All @@ -176,7 +217,7 @@ For REST try using [Resource.delete](/rest/api/createResource#delete)
// deletes MyResource(5)
// this will resuspend MyResource.get({id: '5'})
// and remove it from MyResource.getList
controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' })
controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' });
```

:::
Expand All @@ -199,6 +240,12 @@ function ArticleName({ id }: { id: string }) {
}
```

:::tip

To refresh while continuing to display stale data - [Controller.expireAll](#expireAll) instead.

:::

Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache.

```tsx
Expand Down Expand Up @@ -227,7 +274,7 @@ const managers = [
// call custom unAuth function we defined
unAuth();
// still reset the store
controller.invalidateAll({ testKey })
controller.invalidateAll({ testKey });
},
}),
...CacheProvider.defaultProps.managers,
Expand All @@ -240,7 +287,6 @@ ReactDOM.createRoot(document.body).render(
);
```


## resetEntireStore() {#resetEntireStore}

Resets/clears the entire Reactive Data Client cache. All inflight requests will not resolve.
Expand Down
103 changes: 101 additions & 2 deletions docs/core/concepts/expiry-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,12 @@ render(<Navigator />);

</HooksPlayground>

##

## Force refresh

We sometimes want to fetch new data; while continuing to show the old (stale) data.

### A specific endpoint

[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.

Expand Down Expand Up @@ -329,6 +331,103 @@ render(<ShowTime />);

</HooksPlayground>

### Refresh visible endpoints

[Controller.expireAll()](../api/Controller.md#expireAll) sets all responses' [expiry status](#expiry-status) matching `testKey` to [Stale](#stale).

<HooksPlayground fixtures={[
{
endpoint: new RestEndpoint({
path: '/api/currentTime/:id',
}),
response({ id }) {
return ({
id,
updatedAt: new Date().toISOString(),
});
},
delay: () => 150,
}
]}
>

```ts title="api/lastUpdated" collapsed
export class TimedEntity extends Entity {
id = '';
updatedAt = new Date(0);
pk() {
return this.id;
}

static schema = {
updatedAt: Date,
};
}

export const lastUpdated = new RestEndpoint({
path: '/api/currentTime/:id',
schema: TimedEntity,
});
```

```tsx title="ShowTime" collapsed
import { lastUpdated } from './api/lastUpdated';

export default function ShowTime({ id }: { id: string }) {
const { updatedAt } = useSuspense(lastUpdated, { id });
const ctrl = useController();
return (
<div>
<b>{id}</b>{' '}
<time>
{Intl.DateTimeFormat('en-US', { timeStyle: 'long' }).format(updatedAt)}
</time>
</div>
);
}
```

```tsx title="Loading" collapsed
export default function Loading({ id }: { id: string }) {
return <div>{id} Loading...</div>;
}
```

```tsx title="Demo"
import { AsyncBoundary } 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 />);
```

</HooksPlayground>

## Invalidate (re-suspend) {#invalidate}

Both endpoints and entities can be targetted to be invalidated.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export const SUBSCRIBE_TYPE = 'rest-hooks/subscribe' as const;
export const UNSUBSCRIBE_TYPE = 'rest-hook/unsubscribe' as const;
export const INVALIDATE_TYPE = 'rest-hooks/invalidate' as const;
export const INVALIDATEALL_TYPE = 'rest-hooks/invalidateall' as const;
export const EXPIREALL_TYPE = 'rest-hooks/expireall' as const;
export const GC_TYPE = 'rest-hooks/gc' as const;
10 changes: 10 additions & 0 deletions packages/core/src/controller/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '@data-client/normalizr';
import { inferResults, validateInference } from '@data-client/normalizr';

import createExpireAll from './createExpireAll.js';
import createFetch from './createFetch.js';
import createInvalidate from './createInvalidate.js';
import createInvalidateAll from './createInvalidateAll.js';
Expand Down Expand Up @@ -136,10 +137,19 @@ export default class Controller<
/**
* Forces refetching and suspense on useSuspense on all matching endpoint result keys.
* @see https://resthooks.io/docs/api/Controller#invalidateAll
* @returns Promise that resolves when invalidation is commited.
*/
invalidateAll = (options: { testKey: (key: string) => boolean }) =>
this.dispatch(createInvalidateAll((key: string) => options.testKey(key)));

/**
* Sets all matching endpoint result keys to be STALE.
* @see https://dataclient.io/docs/api/Controller#expireAll
* @returns Promise that resolves when expiry is commited. *NOT* fetch promise
*/
expireAll = (options: { testKey: (key: string) => boolean }) =>
this.dispatch(createExpireAll((key: string) => options.testKey(key)));

/**
* Resets the entire Rest Hooks cache. All inflight requests will not resolve.
* @see https://resthooks.io/docs/api/Controller#resetEntireStore
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/controller/createExpireAll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { EXPIREALL_TYPE } from '../actionTypes.js';
import type { ExpireAllAction } from '../types.js';

export default function createExpireAll(
testKey: (key: string) => boolean,
): ExpireAllAction {
return {
type: EXPIREALL_TYPE,
testKey,
};
}
8 changes: 8 additions & 0 deletions packages/core/src/newActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
GC_TYPE,
OPTIMISTIC_TYPE,
INVALIDATEALL_TYPE,
EXPIREALL_TYPE,
} from './actionTypes.js';
import type { EndpointUpdateFunction } from './controller/types.js';

Expand Down Expand Up @@ -112,6 +113,12 @@ export interface UnsubscribeAction<
};
}

/* EXPIRY */
export interface ExpireAllAction {
type: typeof EXPIREALL_TYPE;
testKey: (key: string) => boolean;
}

/* INVALIDATE */
export interface InvalidateAllAction {
type: typeof INVALIDATEALL_TYPE;
Expand Down Expand Up @@ -146,5 +153,6 @@ export type ActionTypes =
| UnsubscribeAction
| InvalidateAction
| InvalidateAllAction
| ExpireAllAction
| ResetAction
| GCAction;
5 changes: 5 additions & 0 deletions packages/core/src/state/reducer/createReducer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expireReducer } from './expireReducer.js';
import { fetchReducer } from './fetchReducer.js';
import { invalidateReducer } from './invalidateReducer.js';
import { setReducer } from './setReducer.js';
Expand All @@ -9,6 +10,7 @@ import {
GC_TYPE,
OPTIMISTIC_TYPE,
INVALIDATEALL_TYPE,
EXPIREALL_TYPE,
} from '../../actionTypes.js';
import type Controller from '../../controller/Controller.js';
import type { ActionTypes, State } from '../../types.js';
Expand Down Expand Up @@ -43,6 +45,9 @@ export default function createReducer(controller: Controller): ReducerType {
case INVALIDATE_TYPE:
return invalidateReducer(state, action);

case EXPIREALL_TYPE:
return expireReducer(state, action);

case RESET_TYPE:
return { ...initialState, lastReset: action.date };

Expand Down
Loading