Skip to content

Commit ea46e7a

Browse files
authored
Merge pull request #195 from jason89521/feat/staleTime
2 parents b517f8d + c6eecef commit ea46e7a

9 files changed

Lines changed: 127 additions & 22 deletions

File tree

packages/daxus/src/__tests__/useAccessor-revalidateIfStale.test.tsx

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,72 @@
1-
import { waitFor } from '@testing-library/react';
1+
import { act, fireEvent, renderHook, waitFor } from '@testing-library/react';
22
import { useAccessor } from '../index.js';
3-
import { createControl, createPost, createPostModel, renderWithOptionsProvider } from './utils.js';
3+
import {
4+
createControl,
5+
createPost,
6+
createPostModel,
7+
renderWithOptionsProvider,
8+
sleep,
9+
} from './utils.js';
10+
import { useState } from 'react';
411

512
describe('useAccessor-normal revalidateIfStale', () => {
6-
test('should revalidate if the data is marked as stale', async () => {
13+
test('should revalidate if the accessor is stale', async () => {
14+
const fetchDataMock = vi.fn();
15+
const control = createControl({ fetchDataMock });
16+
const { getPostById, postAdapter } = createPostModel(control);
17+
function Page() {
18+
const [id, setId] = useState(0);
19+
const { data } = useAccessor(getPostById(id), postAdapter.tryReadOneFactory(id), {
20+
revalidateIfStale: true,
21+
});
22+
23+
return (
24+
<div>
25+
<button onClick={() => setId(id ? 0 : 1)}>change id</button>
26+
{data?.title}
27+
</div>
28+
);
29+
}
30+
31+
const { getByText, findByText } = renderWithOptionsProvider(<Page />);
32+
await findByText('title0');
33+
fireEvent.click(getByText('change id'));
34+
await findByText('title1');
35+
expect(fetchDataMock).toHaveBeenCalledTimes(2);
36+
fireEvent.click(getByText('change id'));
37+
getByText('title0');
38+
await waitFor(() => {
39+
expect(fetchDataMock).toHaveBeenCalledTimes(3);
40+
});
41+
});
42+
43+
test('should mark accessor as stale after staleTime', async () => {
44+
const control = createControl({});
45+
const { getPostById, postAdapter } = createPostModel(control);
46+
const { result } = renderHook(() =>
47+
useAccessor(getPostById(0), postAdapter.tryReadOneFactory(0), { staleTime: 100 })
48+
);
49+
50+
expect(result.current.accessor.isStale()).toBe(false);
51+
await sleep(50);
52+
expect(result.current.accessor.isStale()).toBe(false);
53+
await waitFor(() => {
54+
expect(result.current.accessor.isStale()).toBe(true);
55+
});
56+
57+
await act(() => result.current.accessor.revalidate());
58+
expect(result.current.accessor.isStale()).toBe(false);
59+
await sleep(50);
60+
// refetch the data. This action should postpone the stale state being set to true.
61+
await act(() => result.current.accessor.revalidate());
62+
await sleep(60);
63+
expect(result.current.accessor.isStale()).toBe(false);
64+
await waitFor(() => {
65+
expect(result.current.accessor.isStale()).toBe(true);
66+
});
67+
});
68+
69+
test('should revalidate if the accessor is invalidated, and it is mounted', async () => {
770
const fetchDataMock = vi.fn();
871
const control = createControl({ fetchDataMock });
972
const { getPostById, postAdapter, postModel } = createPostModel(control);

packages/daxus/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export const defaultOptions: RequiredAccessorOptions<any> = {
1414
keepPreviousData: false,
1515
placeholderData: undefined,
1616
pollingWhenHidden: false,
17+
staleTime: 0,
1718
};

packages/daxus/src/hooks/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface AccessorOptions<S = any> {
5656
keepPreviousData?: boolean;
5757
placeholderData?: S;
5858
pollingWhenHidden?: boolean;
59+
staleTime?: number;
5960
}
6061

6162
export interface AutoAccessorOptions<D, S = any> extends AccessorOptions<S> {

packages/daxus/src/hooks/useAccessor.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ export function useAccessor<S, D, SS, E = unknown>(
146146
() => memoizedSnapshot,
147147
] as const;
148148
// We assume the `getSnapshot` is depending on `accessor` so we don't put it in the dependencies array.
149-
}, [accessor, stateDeps, serverStateKey]);
149+
// ServerStateKey doesn't matter when we are in the client side.
150+
}, [accessor, stateDeps]);
150151

151152
const data = useSyncExternalStore(subscribeData, getData, getData);
152153
const hasData = !isUndefined(data) ? checkHasData(data) : false;
@@ -155,7 +156,7 @@ export function useAccessor<S, D, SS, E = unknown>(
155156
// Always revalidate when this hook is mounted.
156157
if (revalidateOnMount) return true;
157158
// If the accessor is stale, then we should revalidate.
158-
if (revalidateIfStale && accessor?.getIsStale()) return true;
159+
if (revalidateIfStale && accessor?.isStale()) return true;
159160
// If there is no data, we should fetch the data.
160161
if (!hasData) return true;
161162
// This condition is useful when `pollingInterval` changes.

packages/daxus/src/model/Accessor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export class Accessor<S, Arg = any, Data = any, E = unknown> extends BaseAccesso
2525
onMount,
2626
onUnmount,
2727
isAuto,
28+
setStaleTime,
29+
getIsStale,
2830
}: NormalConstructorArgs<S, Arg, Data, E>) {
2931
super({
3032
getState,
@@ -35,6 +37,8 @@ export class Accessor<S, Arg = any, Data = any, E = unknown> extends BaseAccesso
3537
creatorName: action.name,
3638
notifyModel,
3739
isAuto,
40+
setStaleTime,
41+
getIsStale,
3842
});
3943
this.action = action;
4044
this.arg = arg;

packages/daxus/src/model/BaseAccessor.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ export abstract class BaseAccessor<S, Arg, D, E> {
4343
private removeOnReconnectListener: (() => void) | null = null;
4444
private removeOnVisibilityChangeListener: (() => void) | null = null;
4545
private pollingTimeoutId: number | undefined;
46-
private isStale = false;
4746
private onMount: () => void;
4847
private onUnmount: () => void;
4948
private autoListeners: (() => void)[] = [];
5049
private removeAllListeners: (() => void) | null = null;
5150
private isAuto: boolean;
51+
private setStaleTime: (staleTime: number) => void;
5252

5353
/**
5454
* Return the result of the revalidation.
@@ -61,6 +61,7 @@ export abstract class BaseAccessor<S, Arg, D, E> {
6161
* Get the state of the corresponding model.
6262
*/
6363
getState: (serverStateKey?: object) => S;
64+
isStale: () => boolean;
6465

6566
/**
6667
* @internal
@@ -74,10 +75,9 @@ export abstract class BaseAccessor<S, Arg, D, E> {
7475
arg,
7576
isAuto,
7677
creatorName,
77-
}: Pick<
78-
BaseConstructorArgs<S, Arg>,
79-
'getState' | 'modelSubscribe' | 'onMount' | 'onUnmount' | 'arg' | 'notifyModel' | 'isAuto'
80-
> & { creatorName: string }) {
78+
setStaleTime,
79+
getIsStale,
80+
}: Omit<BaseConstructorArgs<S, Arg>, 'updateState'> & { creatorName: string }) {
8181
this.getState = getState;
8282
this.modelSubscribe = modelSubscribe;
8383
this.onMount = onMount;
@@ -86,6 +86,8 @@ export abstract class BaseAccessor<S, Arg, D, E> {
8686
this.arg = arg;
8787
this.isAuto = isAuto;
8888
this.creatorName = creatorName;
89+
this.isStale = getIsStale;
90+
this.setStaleTime = setStaleTime;
8991
}
9092

9193
getIsAuto = () => {
@@ -153,17 +155,8 @@ export abstract class BaseAccessor<S, Arg, D, E> {
153155
return this.status;
154156
};
155157

156-
/**
157-
* Get whether this accessor is stale or not.
158-
*
159-
* @internal
160-
*/
161-
getIsStale = () => {
162-
return this.isStale;
163-
};
164-
165158
invalidate = () => {
166-
this.isStale = true;
159+
this.setStaleTime(0);
167160
if (this.isMounted()) {
168161
this.revalidate();
169162
}
@@ -272,12 +265,12 @@ export abstract class BaseAccessor<S, Arg, D, E> {
272265
error,
273266
data,
274267
}: OnFetchingFinishContext<D, E>): FetchPromiseResult<E, D> => {
275-
const { pollingInterval } = this.getOptions();
268+
const { pollingInterval, staleTime } = this.getOptions();
276269
if (pollingInterval > 0) {
277270
this.pollingTimeoutId = window.setTimeout(this.invokePollingRevalidation, pollingInterval);
278271
}
279272

280-
this.isStale = true;
273+
this.setStaleTime(staleTime);
281274
if (error) {
282275
this.action.onError?.({ error, arg: this.arg });
283276
this.updateStatus({ isFetching: false, error });

packages/daxus/src/model/InfiniteAccessor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export class InfiniteAccessor<S, Arg = any, Data = any, E = unknown> extends Bas
3737
onUnmount,
3838
initialPageNum,
3939
isAuto,
40+
setStaleTime,
41+
getIsStale,
4042
}: InfiniteConstructorArgs<S, Arg, Data, E>) {
4143
super({
4244
getState,
@@ -47,6 +49,8 @@ export class InfiniteAccessor<S, Arg = any, Data = any, E = unknown> extends Bas
4749
creatorName: action.name,
4850
notifyModel,
4951
isAuto,
52+
setStaleTime,
53+
getIsStale,
5054
});
5155
this.action = action;
5256
this.updateState = updateState;

packages/daxus/src/model/Model.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ export function createModel<S extends object>(
104104
NormalAccessorCreator<S, any, any, any> | InfiniteAccessorCreator<S, any, any, any>
105105
>;
106106

107+
// Since accessors may be deleted from cache, we need to save the stale time in the model.
108+
const staleTimeoutIdRecord = {} as Record<string, number>;
109+
const isStaleRecord = {} as Record<string, boolean>;
110+
107111
function assertDuplicateName(name: string) {
108112
if (objectKeys(creatorRecord).includes(name)) {
109113
throw new Error(`The creator name: ${name} has already existed!`);
@@ -199,6 +203,22 @@ export function createModel<S extends object>(
199203
onMount,
200204
onUnmount,
201205
isAuto: action.isAuto ?? false,
206+
setStaleTime(staleTime) {
207+
const timeoutId = staleTimeoutIdRecord[key];
208+
clearTimeout(timeoutId);
209+
// reset the stale state
210+
isStaleRecord[key] = false;
211+
if (staleTime === 0) {
212+
isStaleRecord[key] = true;
213+
} else if (staleTime > 0) {
214+
staleTimeoutIdRecord[key] = window.setTimeout(() => {
215+
isStaleRecord[key] = true;
216+
}, staleTime);
217+
}
218+
},
219+
getIsStale() {
220+
return isStaleRecord[key] ?? false;
221+
},
202222
};
203223

204224
if (isServer()) {
@@ -274,6 +294,22 @@ export function createModel<S extends object>(
274294
onUnmount,
275295
initialPageNum: infiniteAccessorPageNumRecord[key] ?? 1,
276296
isAuto: action.isAuto ?? false,
297+
setStaleTime(staleTime) {
298+
const timeoutId = staleTimeoutIdRecord[key];
299+
clearTimeout(timeoutId);
300+
// reset the stale state
301+
isStaleRecord[key] = false;
302+
if (staleTime === 0) {
303+
isStaleRecord[key] = true;
304+
} else if (staleTime > 0) {
305+
staleTimeoutIdRecord[key] = window.setTimeout(() => {
306+
isStaleRecord[key] = true;
307+
}, staleTime);
308+
}
309+
},
310+
getIsStale() {
311+
return isStaleRecord[key] ?? false;
312+
},
277313
};
278314

279315
if (isServer()) {

packages/daxus/src/model/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export interface BaseConstructorArgs<S, Arg> {
4848
notifyModel: () => void;
4949
onMount: () => void;
5050
onUnmount: () => void;
51+
setStaleTime: (staleTime: number) => void;
52+
getIsStale: () => boolean;
5153
isAuto: boolean;
5254
}
5355

0 commit comments

Comments
 (0)