diff --git a/.changeset/blue-pans-make.md b/.changeset/blue-pans-make.md
new file mode 100644
index 0000000..aab6dcc
--- /dev/null
+++ b/.changeset/blue-pans-make.md
@@ -0,0 +1,5 @@
+---
+"mobx-tanstack-query": patch
+---
+
+remove a lot of useless reactions (replaced it by more simple callbacks)
diff --git a/.changeset/every-parks-march.md b/.changeset/every-parks-march.md
new file mode 100644
index 0000000..b6e3766
--- /dev/null
+++ b/.changeset/every-parks-march.md
@@ -0,0 +1,5 @@
+---
+"mobx-tanstack-query": minor
+---
+
+added `lazy` option for queries and mutations which work on lazy observables from mobx
diff --git a/docs/api/InfiniteQuery.md b/docs/api/InfiniteQuery.md
index d4df6f5..00397e0 100644
--- a/docs/api/InfiniteQuery.md
+++ b/docs/api/InfiniteQuery.md
@@ -4,6 +4,8 @@ Class wrapper for [@tanstack-query/core infinite queries](https://tanstack.com/q
[_See docs for Query_](/api/Query)
+**All documentation about properties and methods of infinite query can be found in the original documentation [here](https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery)**
+
[Reference to source code](/src/infinite-query.ts)
## Usage
diff --git a/docs/api/Mutation.md b/docs/api/Mutation.md
index 6243921..a6aa97d 100644
--- a/docs/api/Mutation.md
+++ b/docs/api/Mutation.md
@@ -1,10 +1,12 @@
-# Mutation
+# Mutation
-Class wrapper for [@tanstack-query/core mutations](https://tanstack.com/query/latest/docs/framework/react/guides/mutations) with **MobX** reactivity
+Class wrapper for [@tanstack-query/core mutations](https://tanstack.com/query/latest/docs/framework/react/guides/mutations) with **MobX** reactivity
-[Reference to source code](/src/mutation.ts)
+**All documentation about properties and methods of mutation can be found in the original documentation [here](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation)**
-## Usage
+[Reference to source code](/src/mutation.ts)
+
+## Usage
Create an instance of `Mutation` with [`mutationFn`](https://tanstack.com/query/latest/docs/framework/react/guides/mutations) parameter
@@ -36,12 +38,13 @@ const petCreateMutation = new Mutation({
const result = await petCreateMutation.mutate('Fluffy');
console.info(result.data, result.isPending, result.isError);
-```
+```
+
+## Built-in Features
-## Built-in Features
+### `abortSignal` option
-### `abortSignal` option
-This field is necessary to kill all reactions and subscriptions that are created during the creation of an instance of the `Mutation` class
+This field is necessary to kill all reactions and subscriptions that are created during the creation of an instance of the `Mutation` class
```ts
const abortController = new AbortController();
@@ -61,27 +64,46 @@ abortController.abort();
This is alternative for `destroy` method
-### `destroy()` method
-This method is necessary to kill all reactions and subscriptions that are created during the creation of an instance of the `Mutation` class
+### `lazy` option
+
+This option enables "lazy" mode of the mutation. That means that all subscriptions and reaction will be created only when you request result for this mutation.
+
+Example:
+
+```ts
+const mutation = createMutation(queryClient, () => ({
+ lazy: true,
+ mutationFn: async () => {
+ // api call
+ },
+}));
+
+// happens nothing
+// no reactions and subscriptions will be created
+```
+
+### `destroy()` method
+
+This method is necessary to kill all reactions and subscriptions that are created during the creation of an instance of the `Mutation` class
-This is alternative for `abortSignal` option
+This is alternative for `abortSignal` option
-### method `mutate(variables, options?)`
-Runs the mutation. (Works the as `mutate` function in [`useMutation` hook](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation))
+### method `mutate(variables, options?)`
-### hook `onDone()`
-Subscribe when mutation has been successfully finished
+Runs the mutation. (Works the as `mutate` function in [`useMutation` hook](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation))
-### hook `onError()`
-Subscribe when mutation has been finished with failure
+### hook `onDone()`
-### method `reset()`
-Reset current mutation
+Subscribe when mutation has been successfully finished
-### property `result`
-Mutation result (The same as returns the [`useMutation` hook](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation))
+### hook `onError()`
+Subscribe when mutation has been finished with failure
+### method `reset()`
+Reset current mutation
+### property `result`
+Mutation result (The same as returns the [`useMutation` hook](https://tanstack.com/query/latest/docs/framework/react/reference/useMutation))
diff --git a/docs/api/Query.md b/docs/api/Query.md
index 2be0845..7c558f7 100644
--- a/docs/api/Query.md
+++ b/docs/api/Query.md
@@ -1,22 +1,27 @@
-# Query
+# Query
-Class wrapper for [@tanstack-query/core queries](https://tanstack.com/query/latest/docs/framework/react/guides/queries) with **MobX** reactivity
+Class wrapper for [@tanstack-query/core queries](https://tanstack.com/query/latest/docs/framework/react/guides/queries) with **MobX** reactivity
-[Reference to source code](/src/query.ts)
+**All documentation about properties and methods of query can be found in the original documentation [here](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery)**
-## Usage
-There are two ways to use queries:
+[Reference to source code](/src/query.ts)
+
+## Usage
+
+There are two ways to use queries:
+
+### 1. Automatic enabling\disabling of queries
-### 1. Automatic enabling\disabling of queries
This approach is suitable when we want the query to automatically make a request and process the data
-depending on the availability of the necessary data.
+depending on the availability of the necessary data.
+
+Example:
-Example:
```ts
const petName = observable.box();
const petQuery = new Query(queryClient, () => ({
- queryKey: ['pets', petName.get()] as const,
+ queryKey: ["pets", petName.get()] as const,
enabled: !!petName.get(), // dynamic
queryFn: async ({ queryKey }) => {
const petName = queryKey[1]!;
@@ -28,19 +33,22 @@ const petQuery = new Query(queryClient, () => ({
// petQuery is not enabled
petQuery.options.enabled;
-petName.set('Fluffy');
+petName.set("Fluffy");
// petQuery is enabled
petQuery.options.enabled;
```
+
### 2. Manual control of query fetching
-This approach is suitable when we need to manually load data using a query.
-Example:
+This approach is suitable when we need to manually load data using a query.
+
+Example:
+
```ts
const petQuery = new Query({
queryClient,
- queryKey: ['pets', undefined as (string | undefined)] as const,
+ queryKey: ["pets", undefined as string | undefined] as const,
enabled: false,
queryFn: async ({ queryKey }) => {
const petName = queryKey[1]!;
@@ -50,11 +58,11 @@ const petQuery = new Query({
});
const result = await petQuery.start({
- queryKey: ['pets', 'Fluffy'],
+ queryKey: ["pets", "Fluffy"],
});
console.log(result.data);
-```
+```
### Another examples
@@ -63,7 +71,7 @@ Create an instance of `Query` with [`queryKey`](https://tanstack.com/query/lates
```ts
const petsQuery = new Query({
queryClient,
- abortSignal, // Helps you to automatically clean up query
+ abortSignal, // Helps you to automatically clean up query or use `lazy` option
queryKey: ['pets'],
queryFn: async ({ signal, queryKey }) => {
const response = await petsApi.fetchPets({ signal });
@@ -77,17 +85,18 @@ console.log(
petsQuery.result.data,
petsQuery.result.isLoading
)
-```
+```
::: info This query is enabled by default!
This means that the query will immediately call the `queryFn` function,
i.e., make a request to `fetchPets`
-This is the default behavior of queries according to the [**query documtation**](https://tanstack.com/query/latest/docs/framework/react/guides/queries)
+This is the default behavior of queries according to the [**query documtation**](https://tanstack.com/query/latest/docs/framework/react/guides/queries)
:::
-## Recommendations
+## Recommendations
+
+### Don't forget about `abortSignal`s or `lazy` option
-### Don't forget about `abortSignal`s
When creating a query, subscriptions to the original queries and reactions are created.
If you don't clean up subscriptions and reactions - memory leaks can occur.
@@ -96,9 +105,10 @@ If you don't clean up subscriptions and reactions - memory leaks can occur.
`queryKey` is not only a cache key but also a way to send necessary data for our API requests!
Example
+
```ts
const petQuery = new Query(queryClient, () => ({
- queryKey: ['pets', 'Fluffy'] as const,
+ queryKey: ["pets", "Fluffy"] as const,
queryFn: async ({ queryKey }) => {
const petName = queryKey[1]!;
const response = await petsApi.getPetByName(petName);
@@ -107,10 +117,11 @@ const petQuery = new Query(queryClient, () => ({
}));
```
-## Built-in Features
+## Built-in Features
+
+### `abortSignal` option
-### `abortSignal` option
-This field is necessary to kill all reactions and subscriptions that are created during the creation of an instance of the `Query` class
+This field is necessary to kill all reactions and subscriptions that are created during the creation of an instance of the `Query` class
```ts
const abortController = new AbortController();
@@ -131,37 +142,43 @@ abortController.abort()
This is alternative for `destroy` method
-### `destroy()` method
-This method is necessary to kill all reactions and subscriptions that are created during the creation of an instance of the `Query` class
+### `destroy()` method
+
+This method is necessary to kill all reactions and subscriptions that are created during the creation of an instance of the `Query` class
-This is alternative for `abortSignal` option
+This is alternative for `abortSignal` option
+
+### `enableOnDemand` option
-### `enableOnDemand` option
Query will be disabled until you request result for this query
-Example:
+Example:
+
```ts
const query = new Query({
//...
- enableOnDemand: true
+ enableOnDemand: true,
});
// happens nothing
query.result.data; // from this code line query starts fetching data
```
-This option works as is if query will be "enabled", otherwise you should enable this query.
+This option works as is if query will be "enabled", otherwise you should enable this query.
+
```ts
const query = new Query({
enabled: false,
- enableOnDemand: true,
+ enableOnDemand: true,
queryFn: () => {},
});
-query.result.data; // nothing happened because query is disabled.
+query.result.data; // nothing happened because query is disabled.
```
-But if you set `enabled` as `true` and option `enableOnDemand` will be `true` too then query will be fetched only after user will try to get access to result.
+
+But if you set `enabled` as `true` and option `enableOnDemand` will be `true` too then query will be fetched only after user will try to get access to result.
+
```ts
const query = new Query({
enabled: true,
- enableOnDemand: true,
+ enableOnDemand: true,
queryFn: () => {},
});
...
@@ -171,117 +188,137 @@ const query = new Query({
query.result.data; // query starts execute the queryFn
```
-### dynamic `options`
-Options which can be dynamically updated for this query
+### dynamic `options`
+
+Options which can be dynamically updated for this query
```ts
const query = new Query({
// ...
options: () => ({
enabled: this.myObservableValue > 10,
- queryKey: ['foo', 'bar', this.myObservableValue] as const,
+ queryKey: ["foo", "bar", this.myObservableValue] as const,
}),
queryFn: ({ queryKey }) => {
const myObservableValue = queryKey[2];
- }
+ },
});
```
-### dynamic `queryKey`
-Works the same as dynamic `options` option but only for `queryKey`
+### dynamic `queryKey`
+
+Works the same as dynamic `options` option but only for `queryKey`
+
```ts
const query = new Query({
// ...
- queryKey: () => ['foo', 'bar', this.myObservableValue] as const,
+ queryKey: () => ["foo", "bar", this.myObservableValue] as const,
queryFn: ({ queryKey }) => {
const myObservableValue = queryKey[2];
- }
+ },
});
-```
-P.S. you can combine it with dynamic (out of box) `enabled` property
+```
+
+P.S. you can combine it with dynamic (out of box) `enabled` property
+
```ts
const query = new Query({
// ...
- queryKey: () => ['foo', 'bar', this.myObservableValue] as const,
+ queryKey: () => ["foo", "bar", this.myObservableValue] as const,
enabled: ({ queryKey }) => queryKey[2] > 10,
queryFn: ({ queryKey }) => {
const myObservableValue = queryKey[2];
- }
+ },
});
-```
-
-### method `start(params)`
+```
-Enable query if it is disabled then fetch the query.
-This method is helpful if you want manually control fetching your query
+### `lazy` option
+This option enables "lazy" mode of the query. That means that all subscriptions and reaction will be created only when you request result for this query.
-Example:
+Example:
```ts
+const query = createQuery(queryClient, () => ({
+ lazy: true,
+ queryKey: ["foo", "bar"] as const,
+ queryFn: async () => {
+ // api call
+ },
+}));
+// happens nothing
+// no reactions and subscriptions will be created
```
+### method `start(params)`
+
+Enable query if it is disabled then fetch the query.
+This method is helpful if you want manually control fetching your query
+
+Example:
-### method `update()`
+```ts
-Update options for query (Uses [QueryObserver](https://tanstack.com/query/latest/docs/reference/QueriesObserver).setOptions)
+```
-### hook `onDone()`
+### method `update()`
-Subscribe when query has been successfully fetched data
+Update options for query (Uses [QueryObserver](https://tanstack.com/query/latest/docs/reference/QueriesObserver).setOptions)
-### hook `onError()`
+### hook `onDone()`
-Subscribe when query has been failed fetched data
+Subscribe when query has been successfully fetched data
-### method `invalidate()`
+### hook `onError()`
-Invalidate current query (Uses [queryClient.invalidateQueries](https://tanstack.com/query/latest/docs/reference/QueryClient/#queryclientinvalidatequeries))
+Subscribe when query has been failed fetched data
-### method `reset()`
+### method `invalidate()`
-Reset current query (Uses [queryClient.resetQueries](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientresetqueries))
+Invalidate current query (Uses [queryClient.invalidateQueries](https://tanstack.com/query/latest/docs/reference/QueryClient/#queryclientinvalidatequeries))
-### method `setData()`
+### method `reset()`
-Set data for current query (Uses [queryClient.setQueryData](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientsetquerydata))
+Reset current query (Uses [queryClient.resetQueries](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientresetqueries))
-### property `isResultRequsted`
-Any time when you trying to get access to `result` property this field sets as `true`
-This field is needed for `enableOnDemand` option
-This property if **observable**
+### method `setData()`
-### property `result`
+Set data for current query (Uses [queryClient.setQueryData](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientsetquerydata))
-**Observable** query result (The same as returns the [`useQuery` hook](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery))
+### property `isResultRequsted`
+Any time when you trying to get access to `result` property this field sets as `true`
+This field is needed for `enableOnDemand` option
+This property if **observable**
+### property `result`
+**Observable** query result (The same as returns the [`useQuery` hook](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery))
-## About `enabled`
-All queries are `enabled` (docs can be found [here](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery)) by default, but you can set `enabled` as `false` or use dynamic value like `({ queryKey }) => !!queryKey[1]`
-You can use `update` method to update value for this property or use dynamic options construction (`options: () => ({ enabled: !!this.observableValue })`)
+## About `enabled`
+All queries are `enabled` (docs can be found [here](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery)) by default, but you can set `enabled` as `false` or use dynamic value like `({ queryKey }) => !!queryKey[1]`
+You can use `update` method to update value for this property or use dynamic options construction (`options: () => ({ enabled: !!this.observableValue })`)
-## About `refetchOnWindowFocus` and `refetchOnReconnect`
+## About `refetchOnWindowFocus` and `refetchOnReconnect`
They **will not work if** you will not call `mount()` method manually of your `QueryClient` instance which you send for your queries, all other cases dependents on query `stale` time and `enabled` properties.
-Example:
+Example:
```ts
-import { hashKey, QueryClient } from '@tanstack/query-core';
+import { hashKey, QueryClient } from "@tanstack/query-core";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: true,
queryKeyHashFn: hashKey,
- refetchOnWindowFocus: 'always',
- refetchOnReconnect: 'always',
+ refetchOnWindowFocus: "always",
+ refetchOnReconnect: "always",
staleTime: 5 * 60 * 1000,
retry: (failureCount, error) => {
- if ('status' in error && Number(error.status) >= 500) {
+ if ("status" in error && Number(error.status) >= 500) {
return failureCount < 3;
}
return false;
@@ -297,4 +334,4 @@ export const queryClient = new QueryClient({
queryClient.mount();
```
-If you work with [`QueryClient`](/api/QueryClient) then calling `mount()` is not needed.
+If you work with [`QueryClient`](/api/QueryClient) then calling `mount()` is not needed.
diff --git a/docs/api/QueryClient.md b/docs/api/QueryClient.md
index 0b31bd3..481be83 100644
--- a/docs/api/QueryClient.md
+++ b/docs/api/QueryClient.md
@@ -1,12 +1,12 @@
# QueryClient
-
An enhanced version of [TanStack's Query QueryClient](https://tanstack.com/query/v5/docs/reference/QueryClient).
-Adds specialized configurations for library entities like [`Query`](/api/Query) or [`Mutation`](/api/Mutation).
+Adds specialized configurations for library entities like [`Query`](/api/Query) or [`Mutation`](/api/Mutation).
+
+[Reference to source code](/src/query-client.ts)
-[Reference to source code](/src/query-client.ts)
+## API Signature
-## API Signature
```ts
import { QueryClient } from "@tanstack/query-core";
@@ -15,10 +15,12 @@ class QueryClient extends QueryClient {
}
```
-## Configuration
+## Configuration
+
When creating an instance, you can provide:
+
```ts
-import { DefaultOptions } from '@tanstack/query-core';
+import { DefaultOptions } from "@tanstack/query-core";
interface QueryClientConfig {
defaultOptions?: DefaultOptions & {
@@ -29,49 +31,86 @@ interface QueryClientConfig {
}
```
-## Key methods and properties
+## Key methods and properties
+
+### `queryFeatures`
+
+Features configurations exclusively for [`Query`](/api/Query)/[`InfiniteQuery`](/api/InfiniteQuery)
+
+Example:
+```ts
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ lazy: true,
+ enableOnDemand: true,
+ // resetOnDestroy: false,
+ // dynamicOptionsUpdateDelay: undefined,
+ throwOnError: true,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ staleTime: 0,
+ retry: false,
+ },
+ },
+});
+```
+
+### `mutationFeatures`
+
+Features configurations exclusively for [`Mutation`](/api/Mutation)
+
+Example:
+```ts
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ mutations: {
+ // invalidateByKey: true,
+ // resetOnDestroy: true,
+ lazy: true,
+ },
+ },
+});
+```
+
+### `hooks`
-### `queryFeatures`
-Features configurations exclusively for [`Query`](/api/Query)/[`InfiniteQuery`](/api/InfiniteQuery)
+Entity lifecycle events. Available hooks:
-### `mutationFeatures`
-Features configurations exclusively for [`Mutation`](/api/Mutation)
+| Hook | Description |
+| ---------------------- | ------------------------------------------------------------------- |
+| onQueryInit | Triggered when a [`Query`](/api/Query) is created |
+| onInfiniteQueryInit | Triggered when a [`InfiniteQuery`](/api/InfiniteQuery) is created |
+| onMutationInit | Triggered when a [`Mutation`](/api/Mutation) is created |
+| onQueryDestroy | Triggered when a [`Query`](/api/Query) is destroyed |
+| onInfiniteQueryDestroy | Triggered when a [`InfiniteQuery`](/api/InfiniteQuery) is destroyed |
+| onMutationDestroy | Triggered when a [`Mutation`](/api/Mutation) is destroyed |
-### `hooks`
-Entity lifecycle events. Available hooks:
+## Inheritance
-| Hook | Description |
-|---|---|
-| onQueryInit | Triggered when a [`Query`](/api/Query) is created |
-| onInfiniteQueryInit | Triggered when a [`InfiniteQuery`](/api/InfiniteQuery) is created |
-| onMutationInit | Triggered when a [`Mutation`](/api/Mutation) is created |
-| onQueryDestroy | Triggered when a [`Query`](/api/Query) is destroyed |
-| onInfiniteQueryDestroy | Triggered when a [`InfiniteQuery`](/api/InfiniteQuery) is destroyed |
-| onMutationDestroy | Triggered when a [`Mutation`](/api/Mutation) is destroyed |
+`QueryClient` inherits all methods and properties from [QueryClient](https://tanstack.com/query/v5/docs/reference/QueryClient), including:
-## Inheritance
-`QueryClient` inherits all methods and properties from [QueryClient](https://tanstack.com/query/v5/docs/reference/QueryClient), including:
-- [`getQueryData()`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientgetquerydata)
+- [`getQueryData()`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientgetquerydata)
- [`setQueryData()`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientsetquerydata)
- [`invalidateQueries()`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientinvalidatequeries)
- [`prefetchQuery()`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientprefetchquery)
- [`cancelQueries()`](https://tanstack.com/query/v5/docs/reference/QueryClient#queryclientcancelqueries)
-- And others ([see official documentation](https://tanstack.com/query/v5/docs/reference/QueryClient))
+- And others ([see official documentation](https://tanstack.com/query/v5/docs/reference/QueryClient))
## Usage Example
```ts
-import { QueryClient } from 'mobx-tanstack-query';
+import { QueryClient } from "mobx-tanstack-query";
// Create a client with custom hooks
const client = new QueryClient({
hooks: {
onQueryInit: (query) => {
- console.log('[Init] Query:', query.queryKey);
+ console.log("[Init] Query:", query.queryKey);
},
onMutationDestroy: (mutation) => {
- console.log('[Destroy] Mutation:', mutation.options.mutationKey);
- }
+ console.log("[Destroy] Mutation:", mutation.options.mutationKey);
+ },
},
defaultOptions: {
queries: {
@@ -79,31 +118,31 @@ const client = new QueryClient({
},
mutations: {
invalidateByKey: true,
- }
+ },
},
});
// Use standard QueryClient methods
-const data = client.getQueryData(['todos']);
+const data = client.getQueryData(["todos"]);
```
## When to Use?
-Use `QueryClient` if you need:
+
+Use `QueryClient` if you need:
+
- Customization of query/mutation lifecycle
- Tracking entity initialization/destruction events
- Advanced configuration for `MobX`-powered queries and mutations.
+## Persistence
-## Persistence
-
-If you need persistence you can use built-in TanStack query feature like `createSyncStoragePersister` or `createAsyncStoragePersister`
-Follow this guide from original TanStack query documentation:
-https://tanstack.com/query/latest/docs/framework/react/plugins/createSyncStoragePersister#api
+If you need persistence you can use built-in TanStack query feature like `createSyncStoragePersister` or `createAsyncStoragePersister`
+Follow this guide from original TanStack query documentation:
+https://tanstack.com/query/latest/docs/framework/react/plugins/createSyncStoragePersister#api
+### Example of implementation
-### Example of implementation
-
-1. Install TanStack's persister dependencies:
+1. Install TanStack's persister dependencies:
::: code-group
@@ -121,13 +160,13 @@ yarn add @tanstack/query-async-storage-persister @tanstack/react-query-persist-c
:::
-2. Create `QueryClient` instance and attach "persistence" feature to it
+2. Create `QueryClient` instance and attach "persistence" feature to it
```ts{2,3,4,6,10}
import { QueryClient } from "mobx-tanstack-query";
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
-import { compress, decompress } from 'lz-string'
+import { compress, decompress } from 'lz-string'
export const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: Infinity } },
@@ -142,4 +181,4 @@ persistQueryClient({
}),
maxAge: Infinity,
});
-```
\ No newline at end of file
+```
diff --git a/src/inifinite-query.ts b/src/inifinite-query.ts
index df18c23..31bb6ed 100644
--- a/src/inifinite-query.ts
+++ b/src/inifinite-query.ts
@@ -19,10 +19,14 @@ import {
makeObservable,
observable,
runInAction,
+ onBecomeUnobserved,
+ onBecomeObserved,
} from 'mobx';
import {
InfiniteQueryConfig,
+ InfiniteQueryDoneListener,
+ InfiniteQueryErrorListener,
InfiniteQueryFlattenConfig,
InfiniteQueryInvalidateParams,
InfiniteQueryOptions,
@@ -32,6 +36,8 @@ import {
} from './inifinite-query.types';
import { AnyQueryClient, QueryClientHooks } from './query-client.types';
+const enableHolder = () => false;
+
export class InfiniteQuery<
TQueryFnData = unknown,
TError = DefaultError,
@@ -40,7 +46,7 @@ export class InfiniteQuery<
TQueryKey extends QueryKey = QueryKey,
> implements Disposable
{
- protected abortController: AbortController;
+ protected abortController: LinkedAbortController;
protected queryClient: AnyQueryClient;
protected _result: InfiniteQueryObserverResult;
@@ -68,9 +74,9 @@ export class InfiniteQuery<
TPageParam
>;
- isResultRequsted: boolean;
-
private isEnabledOnResultDemand: boolean;
+ private isResultRequsted: boolean;
+ private isLazy?: boolean;
/**
* This parameter is responsible for holding the enabled value,
@@ -85,6 +91,8 @@ export class InfiniteQuery<
>['enabled'];
private _observerSubscription?: VoidFunction;
private hooks?: QueryClientHooks;
+ private errorListeners: InfiniteQueryErrorListener[];
+ private doneListeners: InfiniteQueryDoneListener[];
constructor(
config: InfiniteQueryConfig<
@@ -147,12 +155,20 @@ export class InfiniteQuery<
this._result = undefined as any;
this.isResultRequsted = false;
this.isEnabledOnResultDemand = config.enableOnDemand ?? false;
+ this.errorListeners = [];
+ this.doneListeners = [];
this.hooks =
'hooks' in this.queryClient ? this.queryClient.hooks : undefined;
+ this.isLazy = this.config.lazy;
- if ('queryFeatures' in queryClient && config.enableOnDemand == null) {
- this.isEnabledOnResultDemand =
- queryClient.queryFeatures.enableOnDemand ?? false;
+ if ('queryFeatures' in queryClient) {
+ if (this.config.lazy === undefined) {
+ this.isLazy = queryClient.queryFeatures.lazy ?? false;
+ }
+ if (config.enableOnDemand === undefined) {
+ this.isEnabledOnResultDemand =
+ queryClient.queryFeatures.enableOnDemand ?? false;
+ }
}
observable.deep(this, '_result');
@@ -164,10 +180,11 @@ export class InfiniteQuery<
makeObservable(this);
- this.options = this.queryClient.defaultQueryOptions({
- ...restOptions,
- ...getDynamicOptions?.(this),
- } as any) as InfiniteQueryOptions<
+ const isQueryKeyDynamic = typeof queryKeyOrDynamicQueryKey === 'function';
+
+ this.options = this.queryClient.defaultQueryOptions(
+ restOptions as any,
+ ) as InfiniteQueryOptions<
TQueryFnData,
TError,
TPageParam,
@@ -177,24 +194,24 @@ export class InfiniteQuery<
this.options.structuralSharing = this.options.structuralSharing ?? false;
- this.processOptions(this.options);
+ const getAllDynamicOptions =
+ getDynamicOptions || isQueryKeyDynamic
+ ? () => {
+ const freshDynamicOptions = {
+ ...getDynamicOptions?.(this),
+ };
- if (typeof queryKeyOrDynamicQueryKey === 'function') {
- this.options.queryKey = queryKeyOrDynamicQueryKey();
-
- reaction(
- () => queryKeyOrDynamicQueryKey(),
- (queryKey) => {
- this.update({
- queryKey,
- });
- },
- {
- signal: this.abortController.signal,
- delay: this.config.dynamicOptionsUpdateDelay,
- },
- );
- } else {
+ if (isQueryKeyDynamic) {
+ freshDynamicOptions.queryKey = queryKeyOrDynamicQueryKey();
+ }
+
+ return freshDynamicOptions;
+ }
+ : undefined;
+
+ if (getAllDynamicOptions) {
+ Object.assign(this.options, getAllDynamicOptions());
+ } else if (!isQueryKeyDynamic) {
this.options.queryKey =
queryKeyOrDynamicQueryKey ?? this.options.queryKey ?? [];
}
@@ -205,47 +222,81 @@ export class InfiniteQuery<
queryClient.getDefaultOptions().queries?.notifyOnChangeProps ??
'all';
+ this.processOptions(this.options);
+
// @ts-expect-error
this.queryObserver = new InfiniteQueryObserver(queryClient, this.options);
// @ts-expect-error
this.updateResult(this.queryObserver.getOptimisticResult(this.options));
- this._observerSubscription = this.queryObserver.subscribe(
- this.updateResult,
- );
+ if (this.isLazy) {
+ let dynamicOptionsDisposeFn: VoidFunction | undefined;
- if (getDynamicOptions) {
- reaction(() => getDynamicOptions(this), this.update, {
- signal: this.abortController.signal,
- delay: this.config.dynamicOptionsUpdateDelay,
+ onBecomeObserved(this, '_result', () => {
+ if (!this._observerSubscription) {
+ if (getAllDynamicOptions) {
+ this.update(getAllDynamicOptions());
+ }
+ this._observerSubscription = this.queryObserver.subscribe(
+ this.updateResult,
+ );
+ if (getAllDynamicOptions) {
+ dynamicOptionsDisposeFn = reaction(
+ getAllDynamicOptions,
+ this.update,
+ {
+ delay: this.config.dynamicOptionsUpdateDelay,
+ signal: config.abortSignal,
+ fireImmediately: true,
+ },
+ );
+ }
+ }
});
- }
- if (this.isEnabledOnResultDemand) {
- reaction(
- () => this.isResultRequsted,
- (isRequested) => {
- if (isRequested) {
- this.update(getDynamicOptions?.(this) ?? {});
- }
- },
- {
+ const cleanup = () => {
+ if (this._observerSubscription) {
+ dynamicOptionsDisposeFn?.();
+ this._observerSubscription();
+ this._observerSubscription = undefined;
+ dynamicOptionsDisposeFn = undefined;
+ config.abortSignal?.removeEventListener('abort', cleanup);
+ }
+ };
+
+ onBecomeUnobserved(this, '_result', cleanup);
+ config.abortSignal?.addEventListener('abort', cleanup);
+ } else {
+ if (isQueryKeyDynamic) {
+ reaction(
+ queryKeyOrDynamicQueryKey,
+ (queryKey) => this.update({ queryKey }),
+ {
+ signal: this.abortController.signal,
+ delay: this.config.dynamicOptionsUpdateDelay,
+ },
+ );
+ }
+ if (getDynamicOptions) {
+ reaction(() => getDynamicOptions(this), this.update, {
signal: this.abortController.signal,
- fireImmediately: true,
- },
+ delay: this.config.dynamicOptionsUpdateDelay,
+ });
+ }
+ this._observerSubscription = this.queryObserver.subscribe(
+ this.updateResult,
);
+ this.abortController.signal.addEventListener('abort', this.handleAbort);
}
if (config.onDone) {
- this.onDone(config.onDone);
+ this.doneListeners.push(config.onDone);
}
if (config.onError) {
- this.onError(config.onError);
+ this.errorListeners.push(config.onError);
}
- this.abortController.signal.addEventListener('abort', this.handleAbort);
-
this.config.onInit?.(this);
this.hooks?.onInfiniteQueryInit?.(this);
}
@@ -316,12 +367,14 @@ export class InfiniteQuery<
// @ts-expect-error
this.queryObserver.setOptions(this.options);
+
+ if (this.isLazy) {
+ this.updateResult(this.queryObserver.getCurrentResult());
+ }
}
private isEnableHolded = false;
- private enableHolder = () => false;
-
private processOptions = (
options: InfiniteQueryOptions<
TQueryFnData,
@@ -338,14 +391,14 @@ export class InfiniteQuery<
// to do this, we hold the original value of the enabled option
// and set enabled to false until the user requests the result (this.isResultRequsted)
if (this.isEnabledOnResultDemand) {
- if (this.isEnableHolded && options.enabled !== this.enableHolder) {
+ if (this.isEnableHolded && options.enabled !== enableHolder) {
this.holdedEnabledOption = options.enabled;
}
if (this.isResultRequsted) {
if (this.isEnableHolded) {
options.enabled =
- this.holdedEnabledOption === this.enableHolder
+ this.holdedEnabledOption === enableHolder
? undefined
: this.holdedEnabledOption;
this.isEnableHolded = false;
@@ -353,16 +406,17 @@ export class InfiniteQuery<
} else {
this.isEnableHolded = true;
this.holdedEnabledOption = options.enabled;
- options.enabled = this.enableHolder;
+ options.enabled = enableHolder;
}
}
};
public get result() {
- if (!this.isResultRequsted) {
+ if (this.isEnabledOnResultDemand && !this.isResultRequsted) {
runInAction(() => {
this.isResultRequsted = true;
});
+ this.update({});
}
return this._result;
}
@@ -370,8 +424,14 @@ export class InfiniteQuery<
/**
* Modify this result so it matches the tanstack query result.
*/
- private updateResult(nextResult: InfiniteQueryObserverResult) {
- this._result = nextResult || {};
+ private updateResult(result: InfiniteQueryObserverResult) {
+ this._result = result || {};
+
+ if (result.isSuccess && !result.error && result.fetchStatus === 'idle') {
+ this.doneListeners.forEach((fn) => fn(result.data!, void 0));
+ } else if (result.error) {
+ this.errorListeners.forEach((fn) => fn(result.error!, void 0));
+ }
}
async refetch(options?: RefetchOptions) {
@@ -408,35 +468,12 @@ export class InfiniteQuery<
} as any);
}
- onDone(onDoneCallback: (data: TData, payload: void) => void): void {
- reaction(
- () => {
- const { error, isSuccess, fetchStatus } = this._result;
- return isSuccess && !error && fetchStatus === 'idle';
- },
- (isDone) => {
- if (isDone) {
- onDoneCallback(this._result.data!, void 0);
- }
- },
- {
- signal: this.abortController.signal,
- },
- );
+ onDone(doneListener: InfiniteQueryDoneListener): void {
+ this.doneListeners.push(doneListener);
}
- onError(onErrorCallback: (error: TError, payload: void) => void): void {
- reaction(
- () => this._result.error,
- (error) => {
- if (error) {
- onErrorCallback(error, void 0);
- }
- },
- {
- signal: this.abortController.signal,
- },
- );
+ onError(errorListener: InfiniteQueryErrorListener): void {
+ this.errorListeners.push(errorListener);
}
async start({
@@ -457,6 +494,9 @@ export class InfiniteQuery<
protected handleAbort = () => {
this._observerSubscription?.();
+ this.doneListeners = [];
+ this.errorListeners = [];
+
this.queryObserver.destroy();
this.isResultRequsted = false;
diff --git a/src/inifinite-query.types.ts b/src/inifinite-query.types.ts
index 010028c..d12712c 100644
--- a/src/inifinite-query.types.ts
+++ b/src/inifinite-query.types.ts
@@ -16,6 +16,16 @@ import {
QueryResetParams,
} from './query.types';
+export type InfiniteQueryErrorListener = (
+ error: TError,
+ payload: void,
+) => void;
+
+export type InfiniteQueryDoneListener = (
+ data: TData,
+ payload: void,
+) => void;
+
export interface InfiniteQueryInvalidateParams extends QueryInvalidateParams {}
/**
@@ -268,8 +278,8 @@ export interface InfiniteQueryConfig<
query: InfiniteQuery,
) => void;
abortSignal?: AbortSignal;
- onDone?: (data: TData, payload: void) => void;
- onError?: (error: TError, payload: void) => void;
+ onDone?: InfiniteQueryDoneListener;
+ onError?: InfiniteQueryErrorListener;
/**
* Dynamic query parameters, when result of this function changed query will be updated
* (reaction -> setOptions)
diff --git a/src/mutation.ts b/src/mutation.ts
index 80feda9..becb466 100644
--- a/src/mutation.ts
+++ b/src/mutation.ts
@@ -6,11 +6,21 @@ import {
MutationOptions,
} from '@tanstack/query-core';
import { LinkedAbortController } from 'linked-abort-controller';
-import { action, makeObservable, observable, reaction } from 'mobx';
+import {
+ action,
+ makeObservable,
+ observable,
+ onBecomeObserved,
+ onBecomeUnobserved,
+} from 'mobx';
import {
MutationConfig,
+ MutationDoneListener,
+ MutationErrorListener,
+ MutationFeatures,
MutationInvalidateQueriesOptions,
+ MutationSettledListener,
} from './mutation.types';
import { AnyQueryClient, QueryClientHooks } from './query-client.types';
@@ -21,7 +31,7 @@ export class Mutation<
TContext = unknown,
> implements Disposable
{
- protected abortController: AbortController;
+ protected abortController: LinkedAbortController;
protected queryClient: AnyQueryClient;
mutationOptions: MutationObserverOptions;
@@ -29,6 +39,18 @@ export class Mutation<
result: MutationObserverResult;
+ private isLazy?: boolean;
+ private isResetOnDestroy?: MutationFeatures['resetOnDestroy'];
+
+ private settledListeners: MutationSettledListener<
+ TData,
+ TError,
+ TVariables,
+ TContext
+ >[];
+ private errorListeners: MutationErrorListener[];
+ private doneListeners: MutationDoneListener[];
+
private _observerSubscription?: VoidFunction;
private hooks?: QueryClientHooks;
@@ -45,21 +67,38 @@ export class Mutation<
this.abortController = new LinkedAbortController(config.abortSignal);
this.queryClient = queryClient;
this.result = undefined as any;
+ this.isLazy = this.config.lazy;
+ this.settledListeners = [];
+ this.errorListeners = [];
+ this.doneListeners = [];
+ this.isResetOnDestroy =
+ this.config.resetOnDestroy ?? this.config.resetOnDispose;
observable.deep(this, 'result');
action.bound(this, 'updateResult');
makeObservable(this);
- const invalidateByKey =
- providedInvalidateByKey ??
- ('mutationFeatures' in queryClient
- ? queryClient.mutationFeatures.invalidateByKey
- : null);
+ let invalidateByKey: MutationFeatures['invalidateByKey'] =
+ providedInvalidateByKey;
+
+ if ('mutationFeatures' in queryClient) {
+ if (providedInvalidateByKey === undefined) {
+ invalidateByKey = queryClient.mutationFeatures.invalidateByKey;
+ }
+ if (this.config.lazy === undefined) {
+ this.isLazy = queryClient.mutationFeatures.lazy;
+ }
+ if (this.isResetOnDestroy === undefined) {
+ this.isResetOnDestroy =
+ queryClient.mutationFeatures.resetOnDestroy ??
+ queryClient.mutationFeatures.resetOnDispose;
+ }
+
+ this.hooks = queryClient.hooks;
+ }
this.mutationOptions = this.queryClient.defaultMutationOptions(restOptions);
- this.hooks =
- 'hooks' in this.queryClient ? this.queryClient.hooks : undefined;
this.mutationObserver = new MutationObserver<
TData,
@@ -76,21 +115,28 @@ export class Mutation<
this.updateResult(this.mutationObserver.getCurrentResult());
- this._observerSubscription = this.mutationObserver.subscribe(
- this.updateResult,
- );
-
- this.abortController.signal.addEventListener('abort', () => {
- this._observerSubscription?.();
+ if (this.isLazy) {
+ onBecomeObserved(this, 'result', () => {
+ if (!this._observerSubscription) {
+ this.updateResult(this.mutationObserver.getCurrentResult());
+ this._observerSubscription = this.mutationObserver.subscribe(
+ this.updateResult,
+ );
+ }
+ });
+ onBecomeUnobserved(this, 'result', () => {
+ if (this._observerSubscription) {
+ this._observerSubscription?.();
+ this._observerSubscription = undefined;
+ }
+ });
+ } else {
+ this._observerSubscription = this.mutationObserver.subscribe(
+ this.updateResult,
+ );
- if (
- config.resetOnDispose ||
- ('mutationFeatures' in queryClient &&
- queryClient.mutationFeatures.resetOnDispose)
- ) {
- this.reset();
- }
- });
+ this.abortController.signal.addEventListener('abort', this.handleAbort);
+ }
if (invalidateQueries) {
this.onDone((data, payload) => {
@@ -143,7 +189,25 @@ export class Mutation<
variables: TVariables,
options?: MutationOptions,
) {
- await this.mutationObserver.mutate(variables, options);
+ if (this.isLazy) {
+ let error: any;
+
+ try {
+ await this.mutationObserver.mutate(variables, options);
+ } catch (error_) {
+ error = error_;
+ }
+
+ const result = this.mutationObserver.getCurrentResult();
+ this.updateResult(result);
+
+ if (error && this.mutationOptions.throwOnError) {
+ throw error;
+ }
+ } else {
+ await this.mutationObserver.mutate(variables, options);
+ }
+
return this.result;
}
@@ -151,93 +215,46 @@ export class Mutation<
variables: TVariables,
options?: MutationOptions,
) {
- await this.mutationObserver.mutate(variables, options);
- return this.result;
+ return await this.mutate(variables, options);
}
/**
* Modify this result so it matches the tanstack query result.
*/
private updateResult(
- nextResult: MutationObserverResult,
+ result: MutationObserverResult,
) {
- this.result = nextResult || {};
+ this.result = result || {};
+
+ if (result.isSuccess && !result.error) {
+ this.doneListeners.forEach((fn) =>
+ fn(result.data!, result.variables!, result.context),
+ );
+ } else if (result.error) {
+ this.errorListeners.forEach((fn) =>
+ fn(result.error!, result.variables!, result.context),
+ );
+ }
+
+ if (!result.isPending && (result.isError || result.isSuccess)) {
+ this.settledListeners.forEach((fn) =>
+ fn(result.data!, result.error, result.variables!, result.context),
+ );
+ }
}
onSettled(
- onSettledCallback: (
- data: TData | undefined,
- error: TError | null,
- variables: TVariables,
- context: TContext | undefined,
- ) => void,
+ listener: MutationSettledListener,
): void {
- reaction(
- () => {
- const { isSuccess, isError, isPending } = this.result;
- return !isPending && (isSuccess || isError);
- },
- (isSettled) => {
- if (isSettled) {
- onSettledCallback(
- this.result.data,
- this.result.error,
- this.result.variables!,
- this.result.context,
- );
- }
- },
- {
- signal: this.abortController.signal,
- },
- );
+ this.settledListeners.push(listener);
}
- onDone(
- onDoneCallback: (
- data: TData,
- payload: TVariables,
- context: TContext | undefined,
- ) => void,
- ): void {
- reaction(
- () => {
- const { error, isSuccess } = this.result;
- return isSuccess && !error;
- },
- (isDone) => {
- if (isDone) {
- onDoneCallback(
- this.result.data!,
- this.result.variables!,
- this.result.context,
- );
- }
- },
- {
- signal: this.abortController.signal,
- },
- );
+ onDone(listener: MutationDoneListener): void {
+ this.doneListeners.push(listener);
}
- onError(
- onErrorCallback: (
- error: TError,
- payload: TVariables,
- context: TContext | undefined,
- ) => void,
- ): void {
- reaction(
- () => this.result.error,
- (error) => {
- if (error) {
- onErrorCallback(error, this.result.variables!, this.result.context);
- }
- },
- {
- signal: this.abortController.signal,
- },
- );
+ onError(listener: MutationErrorListener): void {
+ this.errorListeners.push(listener);
}
reset() {
@@ -247,16 +264,11 @@ export class Mutation<
protected handleAbort = () => {
this._observerSubscription?.();
- let isNeedToReset =
- this.config.resetOnDestroy || this.config.resetOnDispose;
-
- if ('mutationFeatures' in this.queryClient && !isNeedToReset) {
- isNeedToReset =
- this.queryClient.mutationFeatures.resetOnDestroy ||
- this.queryClient.mutationFeatures.resetOnDispose;
- }
+ this.doneListeners = [];
+ this.errorListeners = [];
+ this.settledListeners = [];
- if (isNeedToReset) {
+ if (this.isResetOnDestroy) {
this.reset();
}
diff --git a/src/mutation.types.ts b/src/mutation.types.ts
index 35dbb5b..8c2daf6 100644
--- a/src/mutation.types.ts
+++ b/src/mutation.types.ts
@@ -28,6 +28,13 @@ export interface MutationFeatures {
* Reset mutation when destroy or abort signal is called
*/
resetOnDestroy?: boolean;
+ /**
+ * **EXPERIMENTAL**
+ *
+ * Make all mutation reactions and subscriptions lazy.
+ * They exists only when mutation result is observed.
+ */
+ lazy?: boolean;
}
/**
@@ -61,6 +68,30 @@ export type MobxMutationFunction<
TVariables = unknown,
> = MutationFn;
+export type MutationSettledListener<
+ TData = unknown,
+ TError = DefaultError,
+ TVariables = void,
+ TContext = unknown,
+> = (
+ data: TData | undefined,
+ error: TError | null,
+ variables: TVariables,
+ context: TContext | undefined,
+) => void;
+
+export type MutationErrorListener<
+ TError = DefaultError,
+ TVariables = void,
+ TContext = unknown,
+> = (error: TError, payload: TVariables, context: TContext | undefined) => void;
+
+export type MutationDoneListener<
+ TData = unknown,
+ TVariables = void,
+ TContext = unknown,
+> = (data: TData, payload: TVariables, context: TContext | undefined) => void;
+
export interface MutationConfig<
TData = unknown,
TVariables = void,
diff --git a/src/query.test.ts b/src/query.test.ts
index 7e08f4f..bd71397 100644
--- a/src/query.test.ts
+++ b/src/query.test.ts
@@ -27,7 +27,7 @@ import {
test,
vi,
} from 'vitest';
-import { waitAsync } from 'yummies/async';
+import { sleep, waitAsync } from 'yummies/async';
import { createQuery } from './preset';
import { Query } from './query';
@@ -291,6 +291,28 @@ describe('Query', () => {
query.dispose();
});
+ it('should be DISABLED from default query options (from query client) (lazy:true)', async () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ enabled: false,
+ },
+ },
+ });
+ const query = new QueryMock(
+ {
+ queryKey: ['test', 0 as number] as const,
+ queryFn: () => 100,
+ lazy: true,
+ },
+ queryClient,
+ );
+
+ expect(query.spies.queryFn).toBeCalledTimes(0);
+
+ query.dispose();
+ });
+
it('should be reactive after change queryKey', async () => {
const query = new QueryMock({
queryKey: ['test', 0 as number] as const,
@@ -308,6 +330,24 @@ describe('Query', () => {
query.dispose();
});
+ it('should be reactive after change queryKey (lazy:true)', async () => {
+ const query = new QueryMock({
+ queryKey: ['test', 0 as number] as const,
+ enabled: ({ queryKey }) => queryKey[1] > 0,
+ queryFn: () => 100,
+ lazy: true,
+ });
+
+ query.update({ queryKey: ['test', 1] as const });
+
+ await when(() => !query._rawResult.isLoading);
+
+ expect(query.spies.queryFn).toBeCalledTimes(1);
+ expect(query.spies.queryFn).nthReturnedWith(1, 100);
+
+ query.dispose();
+ });
+
it('should be reactive dependent on another query (runs before declartion)', async () => {
const disabledQuery = new QueryMock({
queryKey: ['test', 0 as number] as const,
@@ -339,6 +379,39 @@ describe('Query', () => {
dependentQuery.dispose();
});
+ it('should be reactive dependent on another query (runs before declartion) (lazy: true)', async () => {
+ const disabledQuery = new QueryMock({
+ queryKey: ['test', 0 as number] as const,
+ enabled: ({ queryKey }) => queryKey[1] > 0,
+ queryFn: () => 100,
+ lazy: true,
+ });
+
+ disabledQuery.update({ queryKey: ['test', 1] as const });
+
+ const dependentQuery = new QueryMock({
+ options: () => ({
+ enabled: !!disabledQuery.options.enabled,
+ queryKey: [...disabledQuery.options.queryKey, 'dependent'],
+ }),
+ queryFn: ({ queryKey }) => queryKey,
+ lazy: true,
+ });
+
+ await when(() => !disabledQuery._rawResult.isLoading);
+ await when(() => !dependentQuery._rawResult.isLoading);
+
+ expect(dependentQuery.spies.queryFn).toBeCalledTimes(1);
+ expect(dependentQuery.spies.queryFn).nthReturnedWith(1, [
+ 'test',
+ 1,
+ 'dependent',
+ ]);
+
+ disabledQuery.dispose();
+ dependentQuery.dispose();
+ });
+
it('should be reactive dependent on another query (runs after declaration)', async () => {
const tempDisabledQuery = new QueryMock({
queryKey: ['test', 0 as number] as const,
@@ -373,6 +446,87 @@ describe('Query', () => {
});
});
+ it('should be reactive dependent on another query (runs after declaration) (updating lazy query)', async () => {
+ const tempDisabledQuery = new QueryMock({
+ queryKey: ['test', 0 as number] as const,
+ enabled: ({ queryKey }) => queryKey[1] > 0,
+ queryFn: () => 100,
+ lazy: true,
+ });
+
+ const dependentQuery = new QueryMock({
+ options: () => ({
+ enabled: !!tempDisabledQuery.options.enabled,
+ queryKey: [...tempDisabledQuery.options.queryKey, 'dependent'],
+ }),
+ queryFn: ({ queryKey }) => queryKey,
+ });
+
+ tempDisabledQuery.update({ queryKey: ['test', 1] as const });
+
+ await when(() => !tempDisabledQuery._rawResult.isLoading);
+ await when(() => !dependentQuery._rawResult.isLoading);
+
+ expect(dependentQuery.spies.queryFn).toBeCalledTimes(1);
+ // результат с 0 потому что options.enabled у первой квери - это функция и
+ // !!tempDisabledQuery.options.enabled будет всегда true
+ expect(dependentQuery.spies.queryFn).nthReturnedWith(1, [
+ 'test',
+ 0,
+ 'dependent',
+ ]);
+
+ tempDisabledQuery.dispose();
+ dependentQuery.dispose();
+ });
+
+ it('should NOT be reactive dependent on another query because lazy queries has not subscriptions', async () => {
+ const tempDisabledQuery = new QueryMock({
+ queryKey: ['test', 0 as number] as const,
+ enabled: ({ queryKey }) => queryKey[1] > 0,
+ queryFn: () => 100,
+ lazy: true,
+ });
+
+ const dependentQuery = new QueryMock({
+ options: () => {
+ return {
+ enabled: !!tempDisabledQuery.options.enabled,
+ queryKey: [...tempDisabledQuery.options.queryKey, 'dependent'],
+ };
+ },
+ queryFn: ({ queryKey }) => {
+ return queryKey;
+ },
+ lazy: true,
+ });
+
+ tempDisabledQuery.update({ queryKey: ['test', 1] as const });
+
+ await sleep(100);
+
+ expect(dependentQuery.spies.queryFn).toBeCalledTimes(0);
+
+ await sleep(100);
+
+ // НО когда мы начнем следить за кверей то все заработает
+ reaction(
+ () => dependentQuery.result.data,
+ () => {},
+ { fireImmediately: true },
+ );
+
+ expect(dependentQuery.spies.queryFn).toBeCalledTimes(1);
+ expect(dependentQuery.spies.queryFn).nthReturnedWith(1, [
+ 'test',
+ 1,
+ 'dependent',
+ ]);
+
+ tempDisabledQuery.dispose();
+ dependentQuery.dispose();
+ });
+
describe('"options" reactive parameter', () => {
it('"options.queryKey" should updates query', async () => {
const boxCounter = observable.box(0);
@@ -835,8 +989,6 @@ describe('Query', () => {
},
} as Record;
- console.info('asdfdsaf', task.name);
-
const query = new QueryMock(
{
queryKey: [task.name, '2'],
@@ -1420,6 +1572,7 @@ describe('Query', () => {
abortController1.abort();
expect(query1.result).toStrictEqual({
+ ...query1.result,
data: undefined,
dataUpdatedAt: 0,
error: null,
@@ -1447,6 +1600,7 @@ describe('Query', () => {
status: 'pending',
});
expect(query2.result).toStrictEqual({
+ ...query2.result,
data: undefined,
dataUpdatedAt: 0,
error: null,
@@ -1475,6 +1629,7 @@ describe('Query', () => {
});
await waitAsync(10);
expect(query1.result).toStrictEqual({
+ ...query1.result,
data: undefined,
dataUpdatedAt: 0,
error: null,
@@ -1502,6 +1657,7 @@ describe('Query', () => {
status: 'pending',
});
expect(query2.result).toStrictEqual({
+ ...query2.result,
data: 'foo',
dataUpdatedAt: query2.result.dataUpdatedAt,
error: null,
@@ -1530,6 +1686,7 @@ describe('Query', () => {
});
await waitAsync(10);
expect(query1.result).toStrictEqual({
+ ...query1.result,
data: undefined,
dataUpdatedAt: 0,
error: null,
@@ -1582,6 +1739,7 @@ describe('Query', () => {
abortController1.abort();
expect(query1.result).toStrictEqual({
+ ...query1.result,
data: undefined,
dataUpdatedAt: 0,
error: null,
@@ -1609,6 +1767,7 @@ describe('Query', () => {
status: 'pending',
});
expect(query2.result).toStrictEqual({
+ ...query2.result,
data: undefined,
dataUpdatedAt: 0,
error: null,
@@ -1637,6 +1796,7 @@ describe('Query', () => {
});
await waitAsync(10);
expect(query1.result).toStrictEqual({
+ ...query1.result,
data: undefined,
dataUpdatedAt: 0,
error: null,
@@ -1664,6 +1824,7 @@ describe('Query', () => {
status: 'pending',
});
expect(query2.result).toStrictEqual({
+ ...query2.result,
data: 'foo',
dataUpdatedAt: query2.result.dataUpdatedAt,
error: null,
@@ -1692,6 +1853,7 @@ describe('Query', () => {
});
await waitAsync(10);
expect(query1.result).toStrictEqual({
+ ...query1.result,
data: undefined,
dataUpdatedAt: 0,
error: null,
diff --git a/src/query.ts b/src/query.ts
index d8dd686..7dc21ee 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -13,6 +13,8 @@ import {
action,
makeObservable,
observable,
+ onBecomeObserved,
+ onBecomeUnobserved,
reaction,
runInAction,
} from 'mobx';
@@ -22,6 +24,8 @@ import { AnyQueryClient, QueryClientHooks } from './query-client.types';
import { QueryOptionsParams } from './query-options';
import {
QueryConfig,
+ QueryDoneListener,
+ QueryErrorListener,
QueryInvalidateParams,
QueryOptions,
QueryResetParams,
@@ -29,6 +33,8 @@ import {
QueryUpdateOptionsAllVariants,
} from './query.types';
+const enableHolder = () => false;
+
export class Query<
TQueryFnData = unknown,
TError = DefaultError,
@@ -37,7 +43,7 @@ export class Query<
TQueryKey extends QueryKey = QueryKey,
> implements Disposable
{
- protected abortController: AbortController;
+ protected abortController: LinkedAbortController;
protected queryClient: AnyQueryClient;
protected _result: QueryObserverResult;
@@ -51,9 +57,9 @@ export class Query<
TQueryKey
>;
- isResultRequsted: boolean;
-
private isEnabledOnResultDemand: boolean;
+ isResultRequsted: boolean;
+ private isLazy?: boolean;
/**
* This parameter is responsible for holding the enabled value,
@@ -68,6 +74,8 @@ export class Query<
>['enabled'];
private _observerSubscription?: VoidFunction;
private hooks?: QueryClientHooks;
+ private errorListeners: QueryErrorListener[];
+ private doneListeners: QueryDoneListener[];
protected config: QueryConfig<
TQueryFnData,
@@ -132,12 +140,20 @@ export class Query<
this._result = undefined as any;
this.isResultRequsted = false;
this.isEnabledOnResultDemand = config.enableOnDemand ?? false;
+ this.errorListeners = [];
+ this.doneListeners = [];
this.hooks =
'hooks' in this.queryClient ? this.queryClient.hooks : undefined;
+ this.isLazy = this.config.lazy;
- if ('queryFeatures' in queryClient && config.enableOnDemand == null) {
- this.isEnabledOnResultDemand =
- queryClient.queryFeatures.enableOnDemand ?? false;
+ if ('queryFeatures' in queryClient) {
+ if (this.config.lazy === undefined) {
+ this.isLazy = queryClient.queryFeatures.lazy ?? false;
+ }
+ if (config.enableOnDemand === undefined) {
+ this.isEnabledOnResultDemand =
+ queryClient.queryFeatures.enableOnDemand ?? false;
+ }
}
observable.deep(this, '_result');
@@ -149,31 +165,30 @@ export class Query<
makeObservable(this);
- this.options = this.queryClient.defaultQueryOptions({
- ...restOptions,
- ...getDynamicOptions?.(this),
- } as any);
+ const isQueryKeyDynamic = typeof queryKeyOrDynamicQueryKey === 'function';
+
+ this.options = this.queryClient.defaultQueryOptions(restOptions as any);
this.options.structuralSharing = this.options.structuralSharing ?? false;
- this.processOptions(this.options);
+ const getAllDynamicOptions =
+ getDynamicOptions || isQueryKeyDynamic
+ ? () => {
+ const freshDynamicOptions = {
+ ...getDynamicOptions?.(this),
+ };
- if (typeof queryKeyOrDynamicQueryKey === 'function') {
- this.options.queryKey = queryKeyOrDynamicQueryKey();
-
- reaction(
- () => queryKeyOrDynamicQueryKey(),
- (queryKey) => {
- this.update({
- queryKey,
- });
- },
- {
- signal: this.abortController.signal,
- delay: this.config.dynamicOptionsUpdateDelay,
- },
- );
- } else {
+ if (isQueryKeyDynamic) {
+ freshDynamicOptions.queryKey = queryKeyOrDynamicQueryKey();
+ }
+
+ return freshDynamicOptions;
+ }
+ : undefined;
+
+ if (getAllDynamicOptions) {
+ Object.assign(this.options, getAllDynamicOptions());
+ } else if (!isQueryKeyDynamic) {
this.options.queryKey =
queryKeyOrDynamicQueryKey ?? this.options.queryKey ?? [];
}
@@ -184,6 +199,8 @@ export class Query<
queryClient.getDefaultOptions().queries?.notifyOnChangeProps ??
'all';
+ this.processOptions(this.options);
+
this.queryObserver = new QueryObserver<
TQueryFnData,
TError,
@@ -194,41 +211,73 @@ export class Query<
this.updateResult(this.queryObserver.getOptimisticResult(this.options));
- this._observerSubscription = this.queryObserver.subscribe(
- this.updateResult,
- );
+ if (this.isLazy) {
+ let dynamicOptionsDisposeFn: VoidFunction | undefined;
- if (getDynamicOptions) {
- reaction(() => getDynamicOptions(this), this.update, {
- signal: this.abortController.signal,
- delay: this.config.dynamicOptionsUpdateDelay,
+ onBecomeObserved(this, '_result', () => {
+ if (!this._observerSubscription) {
+ if (getAllDynamicOptions) {
+ this.update(getAllDynamicOptions());
+ }
+ this._observerSubscription = this.queryObserver.subscribe(
+ this.updateResult,
+ );
+ if (getAllDynamicOptions) {
+ dynamicOptionsDisposeFn = reaction(
+ getAllDynamicOptions,
+ this.update,
+ {
+ delay: this.config.dynamicOptionsUpdateDelay,
+ signal: config.abortSignal,
+ fireImmediately: true,
+ },
+ );
+ }
+ }
});
- }
- if (this.isEnabledOnResultDemand) {
- reaction(
- () => this.isResultRequsted,
- (isRequested) => {
- if (isRequested) {
- this.update(getDynamicOptions?.(this) ?? {});
- }
- },
- {
+ const cleanup = () => {
+ if (this._observerSubscription) {
+ dynamicOptionsDisposeFn?.();
+ this._observerSubscription();
+ this._observerSubscription = undefined;
+ dynamicOptionsDisposeFn = undefined;
+ config.abortSignal?.removeEventListener('abort', cleanup);
+ }
+ };
+
+ onBecomeUnobserved(this, '_result', cleanup);
+ config.abortSignal?.addEventListener('abort', cleanup);
+ } else {
+ if (isQueryKeyDynamic) {
+ reaction(
+ queryKeyOrDynamicQueryKey,
+ (queryKey) => this.update({ queryKey }),
+ {
+ signal: this.abortController.signal,
+ delay: this.config.dynamicOptionsUpdateDelay,
+ },
+ );
+ }
+ if (getDynamicOptions) {
+ reaction(() => getDynamicOptions(this), this.update, {
signal: this.abortController.signal,
- fireImmediately: true,
- },
+ delay: this.config.dynamicOptionsUpdateDelay,
+ });
+ }
+ this._observerSubscription = this.queryObserver.subscribe(
+ this.updateResult,
);
+ this.abortController.signal.addEventListener('abort', this.handleAbort);
}
if (config.onDone) {
- this.onDone(config.onDone);
+ this.doneListeners.push(config.onDone);
}
if (config.onError) {
- this.onError(config.onError);
+ this.errorListeners.push(config.onError);
}
- this.abortController.signal.addEventListener('abort', this.handleAbort);
-
this.config.onInit?.(this);
this.hooks?.onQueryInit?.(this);
}
@@ -298,12 +347,14 @@ export class Query<
this.options = nextOptions;
this.queryObserver.setOptions(this.options);
+
+ if (this.isLazy) {
+ this.updateResult(this.queryObserver.getCurrentResult());
+ }
}
private isEnableHolded = false;
- private enableHolder = () => false;
-
private processOptions = (
options: QueryOptions,
) => {
@@ -314,14 +365,14 @@ export class Query<
// to do this, we hold the original value of the enabled option
// and set enabled to false until the user requests the result (this.isResultRequsted)
if (this.isEnabledOnResultDemand) {
- if (this.isEnableHolded && options.enabled !== this.enableHolder) {
+ if (this.isEnableHolded && options.enabled !== enableHolder) {
this.holdedEnabledOption = options.enabled;
}
if (this.isResultRequsted) {
if (this.isEnableHolded) {
options.enabled =
- this.holdedEnabledOption === this.enableHolder
+ this.holdedEnabledOption === enableHolder
? undefined
: this.holdedEnabledOption;
this.isEnableHolded = false;
@@ -329,18 +380,19 @@ export class Query<
} else {
this.isEnableHolded = true;
this.holdedEnabledOption = options.enabled;
- options.enabled = this.enableHolder;
+ options.enabled = enableHolder;
}
}
};
public get result() {
- if (!this.isResultRequsted) {
+ if (this.isEnabledOnResultDemand && !this.isResultRequsted) {
runInAction(() => {
this.isResultRequsted = true;
});
+ this.update({});
}
- return this._result;
+ return this._result || this.queryObserver.getCurrentResult();
}
/**
@@ -348,6 +400,12 @@ export class Query<
*/
private updateResult(result: QueryObserverResult) {
this._result = result;
+
+ if (result.isSuccess && !result.error && result.fetchStatus === 'idle') {
+ this.doneListeners.forEach((fn) => fn(result.data!, void 0));
+ } else if (result.error) {
+ this.errorListeners.forEach((fn) => fn(result.error!, void 0));
+ }
}
async reset(params?: QueryResetParams) {
@@ -366,42 +424,21 @@ export class Query<
} as any);
}
- onDone(onDoneCallback: (data: TData, payload: void) => void): void {
- reaction(
- () => {
- const { error, isSuccess, fetchStatus } = this._result;
- return isSuccess && !error && fetchStatus === 'idle';
- },
- (isDone) => {
- if (isDone) {
- onDoneCallback(this._result.data!, void 0);
- }
- },
- {
- signal: this.abortController.signal,
- },
- );
+ onDone(doneListener: QueryDoneListener): void {
+ this.doneListeners.push(doneListener);
}
- onError(onErrorCallback: (error: TError, payload: void) => void): void {
- reaction(
- () => this._result.error,
- (error) => {
- if (error) {
- onErrorCallback(error, void 0);
- }
- },
- {
- signal: this.abortController.signal,
- },
- );
+ onError(errorListener: QueryErrorListener): void {
+ this.errorListeners.push(errorListener);
}
protected handleAbort = () => {
this._observerSubscription?.();
+ this.doneListeners = [];
+ this.errorListeners = [];
+
this.queryObserver.destroy();
- this.isResultRequsted = false;
let isNeedToReset =
this.config.resetOnDestroy || this.config.resetOnDispose;
@@ -421,10 +458,6 @@ export class Query<
this.hooks?.onQueryDestroy?.(this);
};
- destroy() {
- this.abortController.abort();
- }
-
async start({
cancelRefetch,
...params
@@ -440,6 +473,10 @@ export class Query<
return await this.refetch({ cancelRefetch });
}
+ destroy() {
+ this.abortController?.abort();
+ }
+
/**
* @deprecated use `destroy`. This method will be removed in next major release
*/
diff --git a/src/query.types.ts b/src/query.types.ts
index e75b356..ee69222 100644
--- a/src/query.types.ts
+++ b/src/query.types.ts
@@ -122,6 +122,13 @@ export interface QueryFeatures {
* @see https://mobx.js.org/reactions.html#delay-_autorun-reaction_
*/
dynamicOptionsUpdateDelay?: number;
+ /**
+ * **EXPERIMENTAL**
+ *
+ * Make all query reactions and subscriptions lazy.
+ * They exists only when query result is observed.
+ */
+ lazy?: boolean;
}
/**
@@ -150,6 +157,16 @@ export type MobxQueryConfigFromFn<
TQueryKey extends QueryKey = QueryKey,
> = QueryConfigFromFn;
+export type QueryErrorListener = (
+ error: TError,
+ payload: void,
+) => void;
+
+export type QueryDoneListener = (
+ data: TData,
+ payload: void,
+) => void;
+
export interface QueryConfig<
TQueryFnData = unknown,
TError = DefaultError,
@@ -185,8 +202,8 @@ export interface QueryConfig<
query: Query,
) => void;
abortSignal?: AbortSignal;
- onDone?: (data: TData, payload: void) => void;
- onError?: (error: TError, payload: void) => void;
+ onDone?: QueryDoneListener;
+ onError?: QueryErrorListener;
/**
* Dynamic query parameters, when result of this function changed query will be updated
* (reaction -> setOptions)