Skip to content

Commit a4649a8

Browse files
committed
feat: add support for queryKey as reactive fn; test: add more tests for mobx query
1 parent 0389dd9 commit a4649a8

File tree

4 files changed

+306
-48
lines changed

4 files changed

+306
-48
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@
4949
"@vitejs/plugin-react-swc": "3.7.2",
5050
"@vitest/coverage-istanbul": "2.1.6",
5151
"eslint": "8.57.0",
52+
"js2me-eslint-config": "1.0.5",
5253
"js2me-exports-post-build-script": "2.0.17",
5354
"jsdom": "25.0.1",
54-
"js2me-eslint-config": "1.0.5",
5555
"typescript": "5.4.5",
5656
"vitest": "2.1.4"
5757
},

src/mobx-inifinite-query.ts

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,57 @@ import {
2626

2727
import { MobxQueryInvalidateParams, MobxQueryResetParams } from './mobx-query';
2828

29+
export interface MobxInfiniteQueryDynamicOptions<
30+
TData,
31+
TError = DefaultError,
32+
TQueryKey extends QueryKey = QueryKey,
33+
TPageParam = unknown,
34+
> extends Partial<
35+
Omit<
36+
InfiniteQueryObserverOptions<
37+
TData,
38+
TError,
39+
InfiniteData<TData>,
40+
InfiniteData<TData>,
41+
TQueryKey,
42+
TPageParam
43+
>,
44+
'queryFn' | 'enabled' | 'queryKeyHashFn'
45+
>
46+
> {
47+
enabled?: boolean;
48+
}
49+
2950
export interface MobxInfiniteQueryConfig<
3051
TData,
3152
TError = DefaultError,
3253
TQueryKey extends QueryKey = QueryKey,
3354
TPageParam = unknown,
3455
> extends Partial<
35-
InfiniteQueryObserverOptions<
36-
TData,
37-
TError,
38-
InfiniteData<TData>,
39-
InfiniteData<TData>,
40-
TQueryKey,
41-
TPageParam
56+
Omit<
57+
InfiniteQueryObserverOptions<
58+
TData,
59+
TError,
60+
InfiniteData<TData>,
61+
InfiniteData<TData>,
62+
TQueryKey,
63+
TPageParam
64+
>,
65+
'queryKey'
4266
>
4367
> {
4468
queryClient: QueryClient;
69+
/**
70+
* TanStack Query manages query caching for you based on query keys.
71+
* Query keys have to be an Array at the top level, and can be as simple as an Array with a single string, or as complex as an array of many strings and nested objects.
72+
* As long as the query key is serializable, and unique to the query's data, you can use it!
73+
*
74+
* **Important:** If you define it as a function then it will be reactively updates query origin key every time
75+
* when observable values inside the function changes
76+
*
77+
* @link https://tanstack.com/query/v4/docs/framework/react/guides/query-keys#simple-query-keys
78+
*/
79+
queryKey?: TQueryKey | (() => TQueryKey);
4580
onInit?: (
4681
query: MobxInfiniteQuery<TData, TError, TQueryKey, TPageParam>,
4782
) => void;
@@ -65,16 +100,7 @@ export interface MobxInfiniteQueryConfig<
65100
NoInfer<TPageParam>
66101
>
67102
>,
68-
) => Partial<
69-
InfiniteQueryObserverOptions<
70-
TData,
71-
TError,
72-
InfiniteData<TData>,
73-
InfiniteData<TData>,
74-
TQueryKey,
75-
TPageParam
76-
>
77-
>;
103+
) => MobxInfiniteQueryDynamicOptions<TData, TError, TQueryKey, TPageParam>;
78104

79105
/**
80106
* Reset query when dispose is called
@@ -130,6 +156,7 @@ export class MobxInfiniteQuery<
130156
abortSignal: outerAbortSignal,
131157
resetOnDispose,
132158
enableOnDemand,
159+
queryKey: queryKeyOrDynamicQueryKey,
133160
...options
134161
}: MobxInfiniteQueryConfig<TData, TError, TQueryKey, TPageParam>) {
135162
this.abortController = new LinkedAbortController(outerAbortSignal);
@@ -155,6 +182,26 @@ export class MobxInfiniteQuery<
155182
...getDynamicOptions?.(this),
156183
};
157184

185+
if (queryKeyOrDynamicQueryKey) {
186+
if (typeof queryKeyOrDynamicQueryKey === 'function') {
187+
mergedOptions.queryKey = queryKeyOrDynamicQueryKey();
188+
189+
reaction(
190+
() => queryKeyOrDynamicQueryKey(),
191+
(queryKey) => {
192+
this.update({
193+
queryKey,
194+
});
195+
},
196+
{
197+
signal: this.abortController.signal,
198+
},
199+
);
200+
} else {
201+
mergedOptions.queryKey = queryKeyOrDynamicQueryKey;
202+
}
203+
}
204+
158205
this.options = queryClient.defaultQueryOptions({
159206
...mergedOptions,
160207
queryKey: (mergedOptions.queryKey ?? []) as TQueryKey,

src/mobx-query.test.ts

Lines changed: 191 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,216 @@
1-
import { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core';
2-
import { reaction, when } from 'mobx';
1+
import {
2+
DefaultError,
3+
QueryClient,
4+
QueryKey,
5+
QueryObserverOptions,
6+
QueryObserverResult,
7+
RefetchOptions,
8+
SetDataOptions,
9+
Updater,
10+
} from '@tanstack/query-core';
11+
import { observable, reaction, runInAction, when } from 'mobx';
312
import { describe, expect, it, vi } from 'vitest';
413

5-
import { MobxQuery, MobxQueryConfig } from './mobx-query';
14+
import {
15+
MobxQuery,
16+
MobxQueryConfig,
17+
MobxQueryInvalidateParams,
18+
} from './mobx-query';
619

7-
describe('MobxQuery', () => {
8-
const testQueryClient = new QueryClient({});
9-
class TestMobxQuery<
10-
TData,
11-
TError = DefaultError,
12-
TQueryKey extends QueryKey = any,
13-
> extends MobxQuery<TData, TError, TQueryKey> {
14-
constructor(
15-
options: Omit<MobxQueryConfig<TData, TError, TQueryKey>, 'queryClient'>,
16-
) {
17-
super({ ...options, queryClient: testQueryClient });
18-
}
20+
class MobxQueryMock<
21+
TData,
22+
TError = DefaultError,
23+
TQueryKey extends QueryKey = any,
24+
> extends MobxQuery<TData, TError, TQueryKey> {
25+
spies = {
26+
queryFn: vi.fn(),
27+
setData: vi.fn(),
28+
update: vi.fn(),
29+
dispose: vi.fn(),
30+
refetch: vi.fn(),
31+
invalidate: vi.fn(),
32+
onDone: vi.fn(),
33+
onError: vi.fn(),
34+
};
35+
36+
constructor(
37+
options: Omit<MobxQueryConfig<TData, TError, TQueryKey>, 'queryClient'>,
38+
) {
39+
super({
40+
...options,
41+
queryClient: new QueryClient({}),
42+
// @ts-ignore
43+
queryFn: vi.fn(options.queryFn),
44+
});
45+
46+
// @ts-ignore
47+
this.spies.queryFn = this.options.queryFn;
48+
49+
this.onDone(this.spies.onDone);
50+
this.onError(this.spies.onError);
1951
}
2052

21-
it('to be defined', () => {
22-
const mobxQuery = new TestMobxQuery({
53+
refetch(
54+
options?: RefetchOptions | undefined,
55+
): Promise<QueryObserverResult<TData, TError>> {
56+
this.spies.refetch(options);
57+
return super.refetch(options);
58+
}
59+
60+
invalidate(params?: MobxQueryInvalidateParams | undefined): Promise<void> {
61+
this.spies.invalidate(params);
62+
return super.invalidate();
63+
}
64+
65+
update(
66+
options: Partial<
67+
QueryObserverOptions<TData, TError, TQueryKey, TData, QueryKey, never>
68+
>,
69+
): void {
70+
this.spies.update(options);
71+
return super.update(options);
72+
}
73+
74+
setData(
75+
updater: Updater<NoInfer<TData> | undefined, NoInfer<TData> | undefined>,
76+
options?: SetDataOptions | undefined,
77+
): void {
78+
this.spies.setData(updater, options);
79+
return super.setData(updater, options);
80+
}
81+
82+
dispose(): void {
83+
this.spies.dispose();
84+
return super.dispose();
85+
}
86+
}
87+
88+
describe('MobxQuery', () => {
89+
it('"result" field to be defined', () => {
90+
const mobxQuery = new MobxQueryMock({
2391
queryKey: ['test'],
2492
queryFn: () => {},
2593
});
26-
expect(mobxQuery).toBeDefined();
94+
expect(mobxQuery.result).toBeDefined();
2795
});
2896

2997
it('"result" field should be reactive', async () => {
3098
let counter = 0;
31-
const mobxQuery = new TestMobxQuery({
99+
const mobxQuery = new MobxQueryMock({
32100
queryKey: ['test'],
33-
queryFn: () => {
34-
return ++counter;
35-
},
101+
queryFn: () => ++counter,
36102
});
37-
const spy = vi.fn();
103+
const reactionSpy = vi.fn();
38104

39105
const dispose = reaction(
40106
() => mobxQuery.result,
41-
(result) => {
42-
spy(result.data);
43-
},
107+
(result) => reactionSpy(result),
44108
);
45109

46110
await when(() => mobxQuery.result.isLoading);
47111

48-
expect(spy).toBeCalledTimes(1);
49-
expect(spy).nthCalledWith(1, 1);
112+
expect(reactionSpy).toBeCalled();
113+
expect(reactionSpy).toBeCalledWith({ ...mobxQuery.result });
50114

51115
dispose();
52116
});
117+
118+
describe('"queryKey" reactive parameter', () => {
119+
it('should rerun queryFn after queryKey change', () => {
120+
const boxCounter = observable.box(0);
121+
const mobxQuery = new MobxQueryMock({
122+
queryFn: ({ queryKey }) => {
123+
return queryKey[1];
124+
},
125+
queryKey: () => ['test', boxCounter.get()] as const,
126+
});
127+
128+
runInAction(() => {
129+
boxCounter.set(1);
130+
});
131+
132+
expect(mobxQuery.spies.queryFn).toBeCalledTimes(2);
133+
expect(mobxQuery.spies.queryFn).nthReturnedWith(1, 0);
134+
});
135+
136+
it('should rerun queryFn after queryKey change', () => {
137+
const boxEnabled = observable.box(false);
138+
const mobxQuery = new MobxQueryMock({
139+
queryFn: () => 10,
140+
queryKey: () => ['test', boxEnabled.get()] as const,
141+
enabled: ({ queryKey }) => queryKey[1],
142+
});
143+
144+
runInAction(() => {
145+
boxEnabled.set(true);
146+
});
147+
148+
expect(mobxQuery.spies.queryFn).toBeCalledTimes(1);
149+
expect(mobxQuery.spies.queryFn).nthReturnedWith(1, 10);
150+
});
151+
});
152+
153+
describe('"options" reactive parameter', () => {
154+
it('"options.queryKey" should updates query', async () => {
155+
const boxCounter = observable.box(0);
156+
let counter = 0;
157+
const mobxQuery = new MobxQueryMock({
158+
queryFn: ({ queryKey }) => {
159+
counter += queryKey[1] * 10;
160+
return counter;
161+
},
162+
options: () => ({
163+
queryKey: ['test', boxCounter.get()] as const,
164+
}),
165+
});
166+
167+
runInAction(() => {
168+
boxCounter.set(1);
169+
});
170+
171+
expect(mobxQuery.spies.queryFn).toBeCalledTimes(2);
172+
expect(mobxQuery.spies.queryFn).nthReturnedWith(1, 0);
173+
expect(mobxQuery.spies.queryFn).nthReturnedWith(2, 10);
174+
});
175+
176+
it('"options.enabled" should change "enabled" statement for query (enabled as boolean in options)', async () => {
177+
const boxEnabled = observable.box(false);
178+
const mobxQuery = new MobxQueryMock({
179+
queryFn: ({ queryKey }) => {
180+
return queryKey[1];
181+
},
182+
options: () => ({
183+
enabled: boxEnabled.get(),
184+
queryKey: ['test', boxEnabled.get() ? 10 : 0] as const,
185+
}),
186+
});
187+
188+
runInAction(() => {
189+
boxEnabled.set(true);
190+
});
191+
192+
expect(mobxQuery.spies.queryFn).toBeCalledTimes(1);
193+
expect(mobxQuery.spies.queryFn).nthReturnedWith(1, 10);
194+
});
195+
196+
it('"options.enabled" should change "enabled" statement for query (enabled as query based fn)', async () => {
197+
const boxEnabled = observable.box(false);
198+
const mobxQuery = new MobxQueryMock({
199+
queryFn: ({ queryKey }) => {
200+
return queryKey[1];
201+
},
202+
enabled: ({ queryKey }) => queryKey[1],
203+
options: () => ({
204+
queryKey: ['test', boxEnabled.get()] as const,
205+
}),
206+
});
207+
208+
runInAction(() => {
209+
boxEnabled.set(true);
210+
});
211+
212+
expect(mobxQuery.spies.queryFn).toBeCalledTimes(1);
213+
expect(mobxQuery.spies.queryFn).nthReturnedWith(1, true);
214+
});
215+
});
53216
});

0 commit comments

Comments
 (0)