Skip to content

Commit bee7713

Browse files
committed
feat: added lazy option for query and mutation; refactor: useless reactions
1 parent 787a062 commit bee7713

File tree

9 files changed

+605
-286
lines changed

9 files changed

+605
-286
lines changed

.changeset/blue-pans-make.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mobx-tanstack-query": patch
3+
---
4+
5+
remove a lot of useless reactions (replaced it by more simple callbacks)

.changeset/every-parks-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mobx-tanstack-query": minor
3+
---
4+
5+
added `lazy` option for queries and mutations which work on lazy observables from mobx

src/inifinite-query.ts

Lines changed: 125 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@ import {
1919
makeObservable,
2020
observable,
2121
runInAction,
22+
onBecomeUnobserved,
23+
onBecomeObserved,
2224
} from 'mobx';
2325

2426
import {
2527
InfiniteQueryConfig,
28+
InfiniteQueryDoneListener,
29+
InfiniteQueryErrorListener,
2630
InfiniteQueryFlattenConfig,
2731
InfiniteQueryInvalidateParams,
2832
InfiniteQueryOptions,
@@ -32,6 +36,8 @@ import {
3236
} from './inifinite-query.types';
3337
import { AnyQueryClient, QueryClientHooks } from './query-client.types';
3438

39+
const enableHolder = () => false;
40+
3541
export class InfiniteQuery<
3642
TQueryFnData = unknown,
3743
TError = DefaultError,
@@ -40,7 +46,7 @@ export class InfiniteQuery<
4046
TQueryKey extends QueryKey = QueryKey,
4147
> implements Disposable
4248
{
43-
protected abortController: AbortController;
49+
protected abortController: LinkedAbortController;
4450
protected queryClient: AnyQueryClient;
4551

4652
protected _result: InfiniteQueryObserverResult<TData, TError>;
@@ -68,9 +74,9 @@ export class InfiniteQuery<
6874
TPageParam
6975
>;
7076

71-
isResultRequsted: boolean;
72-
7377
private isEnabledOnResultDemand: boolean;
78+
private isResultRequsted: boolean;
79+
private isLazy?: boolean;
7480

7581
/**
7682
* This parameter is responsible for holding the enabled value,
@@ -85,6 +91,8 @@ export class InfiniteQuery<
8591
>['enabled'];
8692
private _observerSubscription?: VoidFunction;
8793
private hooks?: QueryClientHooks;
94+
private errorListeners: InfiniteQueryErrorListener<TError>[];
95+
private doneListeners: InfiniteQueryDoneListener<TData>[];
8896

8997
constructor(
9098
config: InfiniteQueryConfig<
@@ -147,12 +155,20 @@ export class InfiniteQuery<
147155
this._result = undefined as any;
148156
this.isResultRequsted = false;
149157
this.isEnabledOnResultDemand = config.enableOnDemand ?? false;
158+
this.errorListeners = [];
159+
this.doneListeners = [];
150160
this.hooks =
151161
'hooks' in this.queryClient ? this.queryClient.hooks : undefined;
162+
this.isLazy = this.config.lazy;
152163

153-
if ('queryFeatures' in queryClient && config.enableOnDemand == null) {
154-
this.isEnabledOnResultDemand =
155-
queryClient.queryFeatures.enableOnDemand ?? false;
164+
if ('queryFeatures' in queryClient) {
165+
if (this.config.lazy === undefined) {
166+
this.isLazy = queryClient.queryFeatures.lazy ?? false;
167+
}
168+
if (config.enableOnDemand === undefined) {
169+
this.isEnabledOnResultDemand =
170+
queryClient.queryFeatures.enableOnDemand ?? false;
171+
}
156172
}
157173

158174
observable.deep(this, '_result');
@@ -164,10 +180,11 @@ export class InfiniteQuery<
164180

165181
makeObservable(this);
166182

167-
this.options = this.queryClient.defaultQueryOptions({
168-
...restOptions,
169-
...getDynamicOptions?.(this),
170-
} as any) as InfiniteQueryOptions<
183+
const isQueryKeyDynamic = typeof queryKeyOrDynamicQueryKey === 'function';
184+
185+
this.options = this.queryClient.defaultQueryOptions(
186+
restOptions as any,
187+
) as InfiniteQueryOptions<
171188
TQueryFnData,
172189
TError,
173190
TPageParam,
@@ -177,24 +194,24 @@ export class InfiniteQuery<
177194

178195
this.options.structuralSharing = this.options.structuralSharing ?? false;
179196

180-
this.processOptions(this.options);
197+
const getAllDynamicOptions =
198+
getDynamicOptions || isQueryKeyDynamic
199+
? () => {
200+
const freshDynamicOptions = {
201+
...getDynamicOptions?.(this),
202+
};
181203

182-
if (typeof queryKeyOrDynamicQueryKey === 'function') {
183-
this.options.queryKey = queryKeyOrDynamicQueryKey();
184-
185-
reaction(
186-
() => queryKeyOrDynamicQueryKey(),
187-
(queryKey) => {
188-
this.update({
189-
queryKey,
190-
});
191-
},
192-
{
193-
signal: this.abortController.signal,
194-
delay: this.config.dynamicOptionsUpdateDelay,
195-
},
196-
);
197-
} else {
204+
if (isQueryKeyDynamic) {
205+
freshDynamicOptions.queryKey = queryKeyOrDynamicQueryKey();
206+
}
207+
208+
return freshDynamicOptions;
209+
}
210+
: undefined;
211+
212+
if (getAllDynamicOptions) {
213+
Object.assign(this.options, getAllDynamicOptions());
214+
} else if (!isQueryKeyDynamic) {
198215
this.options.queryKey =
199216
queryKeyOrDynamicQueryKey ?? this.options.queryKey ?? [];
200217
}
@@ -205,47 +222,81 @@ export class InfiniteQuery<
205222
queryClient.getDefaultOptions().queries?.notifyOnChangeProps ??
206223
'all';
207224

225+
this.processOptions(this.options);
226+
208227
// @ts-expect-error
209228
this.queryObserver = new InfiniteQueryObserver(queryClient, this.options);
210229

211230
// @ts-expect-error
212231
this.updateResult(this.queryObserver.getOptimisticResult(this.options));
213232

214-
this._observerSubscription = this.queryObserver.subscribe(
215-
this.updateResult,
216-
);
233+
if (this.isLazy) {
234+
let dynamicOptionsDisposeFn: VoidFunction | undefined;
217235

218-
if (getDynamicOptions) {
219-
reaction(() => getDynamicOptions(this), this.update, {
220-
signal: this.abortController.signal,
221-
delay: this.config.dynamicOptionsUpdateDelay,
236+
onBecomeObserved(this, '_result', () => {
237+
if (!this._observerSubscription) {
238+
if (getAllDynamicOptions) {
239+
this.update(getAllDynamicOptions());
240+
}
241+
this._observerSubscription = this.queryObserver.subscribe(
242+
this.updateResult,
243+
);
244+
if (getAllDynamicOptions) {
245+
dynamicOptionsDisposeFn = reaction(
246+
getAllDynamicOptions,
247+
this.update,
248+
{
249+
delay: this.config.dynamicOptionsUpdateDelay,
250+
signal: config.abortSignal,
251+
fireImmediately: true,
252+
},
253+
);
254+
}
255+
}
222256
});
223-
}
224257

225-
if (this.isEnabledOnResultDemand) {
226-
reaction(
227-
() => this.isResultRequsted,
228-
(isRequested) => {
229-
if (isRequested) {
230-
this.update(getDynamicOptions?.(this) ?? {});
231-
}
232-
},
233-
{
258+
const cleanup = () => {
259+
if (this._observerSubscription) {
260+
dynamicOptionsDisposeFn?.();
261+
this._observerSubscription();
262+
this._observerSubscription = undefined;
263+
dynamicOptionsDisposeFn = undefined;
264+
config.abortSignal?.removeEventListener('abort', cleanup);
265+
}
266+
};
267+
268+
onBecomeUnobserved(this, '_result', cleanup);
269+
config.abortSignal?.addEventListener('abort', cleanup);
270+
} else {
271+
if (isQueryKeyDynamic) {
272+
reaction(
273+
queryKeyOrDynamicQueryKey,
274+
(queryKey) => this.update({ queryKey }),
275+
{
276+
signal: this.abortController.signal,
277+
delay: this.config.dynamicOptionsUpdateDelay,
278+
},
279+
);
280+
}
281+
if (getDynamicOptions) {
282+
reaction(() => getDynamicOptions(this), this.update, {
234283
signal: this.abortController.signal,
235-
fireImmediately: true,
236-
},
284+
delay: this.config.dynamicOptionsUpdateDelay,
285+
});
286+
}
287+
this._observerSubscription = this.queryObserver.subscribe(
288+
this.updateResult,
237289
);
290+
this.abortController.signal.addEventListener('abort', this.handleAbort);
238291
}
239292

240293
if (config.onDone) {
241-
this.onDone(config.onDone);
294+
this.doneListeners.push(config.onDone);
242295
}
243296
if (config.onError) {
244-
this.onError(config.onError);
297+
this.errorListeners.push(config.onError);
245298
}
246299

247-
this.abortController.signal.addEventListener('abort', this.handleAbort);
248-
249300
this.config.onInit?.(this);
250301
this.hooks?.onInfiniteQueryInit?.(this);
251302
}
@@ -316,12 +367,14 @@ export class InfiniteQuery<
316367

317368
// @ts-expect-error
318369
this.queryObserver.setOptions(this.options);
370+
371+
if (this.isLazy) {
372+
this.updateResult(this.queryObserver.getCurrentResult());
373+
}
319374
}
320375

321376
private isEnableHolded = false;
322377

323-
private enableHolder = () => false;
324-
325378
private processOptions = (
326379
options: InfiniteQueryOptions<
327380
TQueryFnData,
@@ -338,40 +391,47 @@ export class InfiniteQuery<
338391
// to do this, we hold the original value of the enabled option
339392
// and set enabled to false until the user requests the result (this.isResultRequsted)
340393
if (this.isEnabledOnResultDemand) {
341-
if (this.isEnableHolded && options.enabled !== this.enableHolder) {
394+
if (this.isEnableHolded && options.enabled !== enableHolder) {
342395
this.holdedEnabledOption = options.enabled;
343396
}
344397

345398
if (this.isResultRequsted) {
346399
if (this.isEnableHolded) {
347400
options.enabled =
348-
this.holdedEnabledOption === this.enableHolder
401+
this.holdedEnabledOption === enableHolder
349402
? undefined
350403
: this.holdedEnabledOption;
351404
this.isEnableHolded = false;
352405
}
353406
} else {
354407
this.isEnableHolded = true;
355408
this.holdedEnabledOption = options.enabled;
356-
options.enabled = this.enableHolder;
409+
options.enabled = enableHolder;
357410
}
358411
}
359412
};
360413

361414
public get result() {
362-
if (!this.isResultRequsted) {
415+
if (this.isEnabledOnResultDemand && !this.isResultRequsted) {
363416
runInAction(() => {
364417
this.isResultRequsted = true;
365418
});
419+
this.update({});
366420
}
367421
return this._result;
368422
}
369423

370424
/**
371425
* Modify this result so it matches the tanstack query result.
372426
*/
373-
private updateResult(nextResult: InfiniteQueryObserverResult<TData, TError>) {
374-
this._result = nextResult || {};
427+
private updateResult(result: InfiniteQueryObserverResult<TData, TError>) {
428+
this._result = result || {};
429+
430+
if (result.isSuccess && !result.error && result.fetchStatus === 'idle') {
431+
this.doneListeners.forEach((fn) => fn(result.data!, void 0));
432+
} else if (result.error) {
433+
this.errorListeners.forEach((fn) => fn(result.error!, void 0));
434+
}
375435
}
376436

377437
async refetch(options?: RefetchOptions) {
@@ -408,35 +468,12 @@ export class InfiniteQuery<
408468
} as any);
409469
}
410470

411-
onDone(onDoneCallback: (data: TData, payload: void) => void): void {
412-
reaction(
413-
() => {
414-
const { error, isSuccess, fetchStatus } = this._result;
415-
return isSuccess && !error && fetchStatus === 'idle';
416-
},
417-
(isDone) => {
418-
if (isDone) {
419-
onDoneCallback(this._result.data!, void 0);
420-
}
421-
},
422-
{
423-
signal: this.abortController.signal,
424-
},
425-
);
471+
onDone(doneListener: InfiniteQueryDoneListener<TData>): void {
472+
this.doneListeners.push(doneListener);
426473
}
427474

428-
onError(onErrorCallback: (error: TError, payload: void) => void): void {
429-
reaction(
430-
() => this._result.error,
431-
(error) => {
432-
if (error) {
433-
onErrorCallback(error, void 0);
434-
}
435-
},
436-
{
437-
signal: this.abortController.signal,
438-
},
439-
);
475+
onError(errorListener: InfiniteQueryErrorListener<TError>): void {
476+
this.errorListeners.push(errorListener);
440477
}
441478

442479
async start({
@@ -457,6 +494,9 @@ export class InfiniteQuery<
457494
protected handleAbort = () => {
458495
this._observerSubscription?.();
459496

497+
this.doneListeners = [];
498+
this.errorListeners = [];
499+
460500
this.queryObserver.destroy();
461501
this.isResultRequsted = false;
462502

0 commit comments

Comments
 (0)