Skip to content

Commit 407df51

Browse files
committed
Add controller.expireAll() that sets all responses to *STALE*
1 parent af8b760 commit 407df51

File tree

16 files changed

+405
-20
lines changed

16 files changed

+405
-20
lines changed

.changeset/cool-dryers-change.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@data-client/react': patch
3+
---
4+
5+
Update README examples to have more options configured

.changeset/many-pears-explain.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@data-client/react': minor
3+
'@data-client/core': minor
4+
'@rest-hooks/core': minor
5+
'@rest-hooks/react': minor
6+
---
7+
8+
Add controller.expireAll() that sets all responses to *STALE*
9+
10+
```ts
11+
controller.expireAll(ArticleResource.getList);
12+
```
13+
14+
This is like controller.invalidateAll(); but will continue showing
15+
stale data while it is refetched.
16+
17+
This is sometimes useful to trigger refresh of only data presently shown
18+
when there are many parameterizations in cache.

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,16 @@ class Article extends Entity {
7373
const UserResource = createResource({
7474
path: '/users/:id',
7575
schema: User,
76+
optimistic: true,
7677
});
7778

7879
const ArticleResource = createResource({
7980
path: '/articles/:id',
8081
schema: Article,
82+
searchParams: {} as
83+
| { beginAt?: string; endAt?: string; author?: string }
84+
| undefined,
85+
optimistic: true,
8186
});
8287
```
8388

@@ -86,12 +91,12 @@ const ArticleResource = createResource({
8691
```tsx
8792
const article = useSuspense(ArticleResource.get, { id });
8893
return (
89-
<>
94+
<article>
9095
<h2>
9196
{article.title} by {article.author.username}
9297
</h2>
9398
<p>{article.body}</p>
94-
</>
99+
</article>
95100
);
96101
```
97102

docs/core/api/Controller.md

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class Controller {
2424
subscribe(endpoint, ...args): Promise<void>;
2525
unsubscribe(endpoint, ...args): Promise<void>;
2626
/*************** Data Access ***************/
27-
getResponse(endpoint, ...args, state): { data, expiryStatus, expiresAt };
27+
getResponse(endpoint, ...args, state): { data; expiryStatus; expiresAt };
2828
getError(endpoint, ...args, state): ErrorTypes | undefined;
2929
snapshot(state: State<unknown>, fetchedAt?: number): SnapshotInterface;
3030
getState(): State<unknown>;
@@ -56,7 +56,9 @@ function CreatePost() {
5656

5757
return (
5858
<form
59-
onSubmit={e => ctrl.fetch(PostResource.getList.push, new FormData(e.target))}
59+
onSubmit={e =>
60+
ctrl.fetch(PostResource.getList.push, new FormData(e.target))
61+
}
6062
>
6163
{/* ... */}
6264
</form>
@@ -141,6 +143,44 @@ post.pk();
141143
- Identical requests are deduplicated globally; allowing only one inflight request at a time.
142144
- To ensure a _new_ request is started, make sure to abort any existing inflight requests.
143145

146+
## expireAll({ testKey }) {#expireAll}
147+
148+
Sets all responses' [expiry status](../concepts/expiry-policy.md) matching `testKey` to [Stale](../concepts/expiry-policy.md#stale).
149+
150+
This is sometimes useful to trigger refresh of only data presently shown
151+
when there are many parameterizations in cache.
152+
153+
```tsx
154+
import { type Controller, useController } from '@data-client/react';
155+
156+
const createTradeHandler = (ctrl: Controller) => async trade => {
157+
await ctrl.fetch(TradeResource.getList.push({ user: user.id }, trade));
158+
// highlight-start
159+
ctrl.expireAll(AccountResource.get);
160+
ctrl.expireAll(AccountResource.getList);
161+
// highlight-end
162+
};
163+
164+
function CreateTrade({ id }: { id: string }) {
165+
const handleTrade = createTradeHandler(useController());
166+
167+
return (
168+
<Form onSubmit={handleTrade}>
169+
<FormField name="ticker" />
170+
<FormField name="amount" type="number" />
171+
<FormField name="price" type="number" />
172+
</Form>
173+
);
174+
}
175+
```
176+
177+
:::tip
178+
179+
To reduce load, improve performance, and improve state consistency; it can often be
180+
better to [include mutation sideeffects in the mutation response](/rest/guides/rpc).
181+
182+
:::
183+
144184
## invalidate(endpoint, ...args) {#invalidate}
145185

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

163203
:::tip
164204

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

167207
:::
168208

@@ -176,7 +216,7 @@ For REST try using [Resource.delete](/rest/api/createResource#delete)
176216
// deletes MyResource(5)
177217
// this will resuspend MyResource.get({id: '5'})
178218
// and remove it from MyResource.getList
179-
controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' })
219+
controller.setResponse(MyResource.delete, { id: '5' }, { id: '5' });
180220
```
181221

182222
:::
@@ -199,6 +239,12 @@ function ArticleName({ id }: { id: string }) {
199239
}
200240
```
201241

242+
:::tip
243+
244+
To refresh while continuing to display stale data - [Controller.expireAll](#expireAll) instead.
245+
246+
:::
247+
202248
Here we clear only GET endpoints using the test.com domain. This means other domains remain in cache.
203249

204250
```tsx
@@ -227,7 +273,7 @@ const managers = [
227273
// call custom unAuth function we defined
228274
unAuth();
229275
// still reset the store
230-
controller.invalidateAll({ testKey })
276+
controller.invalidateAll({ testKey });
231277
},
232278
}),
233279
...CacheProvider.defaultProps.managers,
@@ -240,7 +286,6 @@ ReactDOM.createRoot(document.body).render(
240286
);
241287
```
242288

243-
244289
## resetEntireStore() {#resetEntireStore}
245290

246291
Resets/clears the entire Reactive Data Client cache. All inflight requests will not resolve.

packages/core/src/actionTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export const SUBSCRIBE_TYPE = 'rest-hooks/subscribe' as const;
88
export const UNSUBSCRIBE_TYPE = 'rest-hook/unsubscribe' as const;
99
export const INVALIDATE_TYPE = 'rest-hooks/invalidate' as const;
1010
export const INVALIDATEALL_TYPE = 'rest-hooks/invalidateall' as const;
11+
export const EXPIREALL_TYPE = 'rest-hooks/expireall' as const;
1112
export const GC_TYPE = 'rest-hooks/gc' as const;

packages/core/src/controller/Controller.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from '@data-client/normalizr';
2020
import { inferResults, validateInference } from '@data-client/normalizr';
2121

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

145+
/**
146+
* Sets all matching endpoint result keys to be STALE.
147+
* @see https://dataclient.io/docs/api/Controller#expireAll
148+
* @returns Promise that resolves when expiry is commited. *NOT* fetch promise
149+
*/
150+
expireAll = (options: { testKey: (key: string) => boolean }) =>
151+
this.dispatch(createExpireAll((key: string) => options.testKey(key)));
152+
143153
/**
144154
* Resets the entire Rest Hooks cache. All inflight requests will not resolve.
145155
* @see https://resthooks.io/docs/api/Controller#resetEntireStore
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { EXPIREALL_TYPE } from '../actionTypes.js';
2+
import type { ExpireAllAction } from '../types.js';
3+
4+
export default function createExpireAll(
5+
testKey: (key: string) => boolean,
6+
): ExpireAllAction {
7+
return {
8+
type: EXPIREALL_TYPE,
9+
testKey,
10+
};
11+
}

packages/core/src/newActions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
GC_TYPE,
1515
OPTIMISTIC_TYPE,
1616
INVALIDATEALL_TYPE,
17+
EXPIREALL_TYPE,
1718
} from './actionTypes.js';
1819
import type { EndpointUpdateFunction } from './controller/types.js';
1920

@@ -112,6 +113,12 @@ export interface UnsubscribeAction<
112113
};
113114
}
114115

116+
/* EXPIRY */
117+
export interface ExpireAllAction {
118+
type: typeof EXPIREALL_TYPE;
119+
testKey: (key: string) => boolean;
120+
}
121+
115122
/* INVALIDATE */
116123
export interface InvalidateAllAction {
117124
type: typeof INVALIDATEALL_TYPE;
@@ -146,5 +153,6 @@ export type ActionTypes =
146153
| UnsubscribeAction
147154
| InvalidateAction
148155
| InvalidateAllAction
156+
| ExpireAllAction
149157
| ResetAction
150158
| GCAction;

packages/core/src/state/reducer/createReducer.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { expireReducer } from './expireReducer.js';
12
import { fetchReducer } from './fetchReducer.js';
23
import { invalidateReducer } from './invalidateReducer.js';
34
import { setReducer } from './setReducer.js';
@@ -9,6 +10,7 @@ import {
910
GC_TYPE,
1011
OPTIMISTIC_TYPE,
1112
INVALIDATEALL_TYPE,
13+
EXPIREALL_TYPE,
1214
} from '../../actionTypes.js';
1315
import type Controller from '../../controller/Controller.js';
1416
import type { ActionTypes, State } from '../../types.js';
@@ -43,6 +45,9 @@ export default function createReducer(controller: Controller): ReducerType {
4345
case INVALIDATE_TYPE:
4446
return invalidateReducer(state, action);
4547

48+
case EXPIREALL_TYPE:
49+
return expireReducer(state, action);
50+
4651
case RESET_TYPE:
4752
return { ...initialState, lastReset: action.date };
4853

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { State, ExpireAllAction } from '../../types.js';
2+
3+
export function expireReducer(state: State<unknown>, action: ExpireAllAction) {
4+
const meta = { ...state.meta };
5+
6+
Object.keys(meta).forEach(key => {
7+
if (action.testKey(key)) {
8+
meta[key] = {
9+
...meta[key],
10+
// 1 instead of 0 so we can do 'falsy' checks to see if it is set
11+
expiresAt: 1,
12+
};
13+
}
14+
});
15+
16+
return {
17+
...state,
18+
meta,
19+
};
20+
}

0 commit comments

Comments
 (0)