Skip to content

Add refetch on changed fn #85

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/mst-query/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/mst-query/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mst-query",
"version": "4.0.5",
"version": "4.1.1",
"description": "Query library for mobx-state-tree",
"source": "src/index.ts",
"type": "module",
Expand Down
26 changes: 22 additions & 4 deletions packages/mst-query/src/MstQueryHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type QueryHookOptions = {
enabled?: boolean;
isMounted?: any;
isRequestEqual?: boolean;
isReenabled?: boolean;
refetchOnMount?: 'always' | 'never' | 'if-stale';
refetchOnChanged?: 'all' | 'request' | 'pagination' | 'none';
};
Expand All @@ -46,7 +47,7 @@ export class DisposedError extends Error {}

export class QueryObserver {
query: any;
options: any;
options: any = {};
isQuery: boolean;
isMounted = false;
isFetchedAfterMount = false;
Expand Down Expand Up @@ -77,13 +78,15 @@ export class QueryObserver {
}

setOptions(options: any) {
this.options = options;

this.subscribe();

if (this.isQuery) {
options.isMounted = this.isMounted;

if (!this.options.enabled && options.enabled) {
options.isReenabled = true;
}

const refetchRequestOnChanged =
options.refetchOnChanged === 'all' || options.refetchOnChanged === 'request';
options.isRequestEqual = true;
Expand All @@ -99,6 +102,19 @@ export class QueryObserver {
}
}

if (options.enabled && typeof options.refetchOnChanged === 'function') {
if (this.query.variables.request && this.query.variables.pagination) {
options.isRequestEqual = !options.refetchOnChanged({
prevRequest: this.query.variables.request,
prevPagination: this.query.variables.pagination,
});
} else if (this.query.variables.request) {
options.isRequestEqual = !options.refetchOnChanged({
prevRequest: this.query.variables.request,
});
}
}

if (options.initialData && !options.isMounted) {
const isStale = isDataStale(options.initialDataUpdatedAt, options.staleTime);
if (!isStale) {
Expand All @@ -118,6 +134,8 @@ export class QueryObserver {
if (!this.isMounted) {
this.isMounted = true;
}

this.options = options;
}
}

Expand Down Expand Up @@ -276,7 +294,7 @@ export class MstQueryHandler {
return this.model.query(options);
}

if (notInitialized && options.refetchOnChanged === 'none') {
if (notInitialized && options.isReenabled) {
return this.model.query(options);
}

Expand Down
50 changes: 36 additions & 14 deletions packages/mst-query/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Instance, SnapshotIn } from 'mobx-state-tree';
import { useContext, useEffect, useRef, useState } from 'react';
import { VolatileQuery, MutationReturnType, QueryReturnType, InfiniteQueryReturnType } from './create';
import {
VolatileQuery,
MutationReturnType,
QueryReturnType,
InfiniteQueryReturnType,
} from './create';
import { Context } from './QueryClientProvider';
import { QueryClient } from './QueryClient';
import { EmptyPagination, EmptyRequest, QueryObserver } from './MstQueryHandler';
Expand All @@ -16,7 +21,12 @@ function mergeWithDefaultOptions(key: string, options: any, queryClient: QueryCl
type QueryOptions<T extends Instance<QueryReturnType>> = {
request?: SnapshotIn<T['variables']['request']>;
refetchOnMount?: 'always' | 'never' | 'if-stale';
refetchOnChanged?: 'all' | 'request' | 'pagination' | 'none';
refetchOnChanged?:
| 'all'
| 'request'
| 'pagination'
| 'none'
| ((options: { prevRequest: Exclude<T['variables']['request'], undefined> }) => boolean);
staleTime?: number;
enabled?: boolean;
initialData?: any;
Expand All @@ -26,7 +36,7 @@ type QueryOptions<T extends Instance<QueryReturnType>> = {

export function useQuery<T extends Instance<QueryReturnType>>(
query: T,
options: QueryOptions<T> = {}
options: QueryOptions<T> = {},
) {
const [observer, setObserver] = useState(() => new QueryObserver(query, true));

Expand All @@ -36,7 +46,9 @@ export function useQuery<T extends Instance<QueryReturnType>>(
(options as any).request = options.request ?? EmptyRequest;

if ((query as any).isInfinite) {
throw new Error('useQuery should be used with a query that does not have pagination. Use useInfiniteQuery instead.');
throw new Error(
'useQuery should be used with a query that does not have pagination. Use useInfiniteQuery instead.',
);
}

useEffect(() => {
Expand All @@ -54,7 +66,7 @@ export function useQuery<T extends Instance<QueryReturnType>>(
}, [options]);

return {
data: query.data as typeof query['data'],
data: query.data as (typeof query)['data'],
dataUpdatedAt: query.__MstQueryHandler.cachedAt?.getTime(),
error: query.error,
isFetched: query.isFetched,
Expand All @@ -71,7 +83,15 @@ type InfiniteQueryOptions<T extends Instance<InfiniteQueryReturnType>> = {
request?: SnapshotIn<T['variables']['request']>;
pagination?: SnapshotIn<T['variables']['pagination']>;
refetchOnMount?: 'always' | 'never' | 'if-stale';
refetchOnChanged?: 'all' | 'request' | 'pagination' | 'none';
refetchOnChanged?:
| 'all'
| 'request'
| 'pagination'
| 'none'
| ((options: {
prevRequest: Exclude<T['variables']['request'], undefined>;
prevPagination: Exclude<T['variables']['pagination'], undefined>;
}) => boolean);
staleTime?: number;
enabled?: boolean;
initialData?: any;
Expand All @@ -81,7 +101,7 @@ type InfiniteQueryOptions<T extends Instance<InfiniteQueryReturnType>> = {

export function useInfiniteQuery<T extends Instance<InfiniteQueryReturnType>>(
query: T,
options: InfiniteQueryOptions<T> = {}
options: InfiniteQueryOptions<T> = {},
) {
const [observer, setObserver] = useState(() => new QueryObserver(query, true));

Expand All @@ -90,9 +110,11 @@ export function useInfiniteQuery<T extends Instance<InfiniteQueryReturnType>>(

(options as any).request = options.request ?? EmptyRequest;
(options as any).pagination = options.pagination ?? EmptyPagination;

if (!(query as any).isInfinite) {
throw new Error('useInfiniteQuery should be used with a query that has pagination. Use useQuery instead.');
throw new Error(
'useInfiniteQuery should be used with a query that has pagination. Use useQuery instead.',
);
}

useEffect(() => {
Expand All @@ -110,7 +132,7 @@ export function useInfiniteQuery<T extends Instance<InfiniteQueryReturnType>>(
}, [options]);

return {
data: query.data as typeof query['data'],
data: query.data as (typeof query)['data'],
dataUpdatedAt: query.__MstQueryHandler.cachedAt?.getTime(),
error: query.error,
isFetched: query.isFetched,
Expand All @@ -131,7 +153,7 @@ type MutationOptions<T extends Instance<MutationReturnType>> = {

export function useMutation<T extends Instance<MutationReturnType>>(
mutation: T,
options: MutationOptions<T> = {}
options: MutationOptions<T> = {},
) {
const [observer, setObserver] = useState(() => new QueryObserver(mutation, false));

Expand All @@ -149,7 +171,7 @@ export function useMutation<T extends Instance<MutationReturnType>>(
}, [options]);

const result = {
data: mutation.data as typeof mutation['data'],
data: mutation.data as (typeof mutation)['data'],
error: mutation.error,
isLoading: mutation.isLoading,
mutation,
Expand All @@ -162,7 +184,7 @@ export function useMutation<T extends Instance<MutationReturnType>>(
}) => {
const result = mutation.mutate({ ...params, ...options } as any);
return result as Promise<{ data: T['data']; error: any; result: TResult }>;
}
},
);

return [mutate, result] as [typeof mutate, typeof result];
Expand All @@ -181,7 +203,7 @@ type UseVolatileQueryOptions<T extends Instance<QueryReturnType>> = QueryOptions
};

export function useVolatileQuery(
options: UseVolatileQueryOptions<Instance<typeof VolatileQuery>> = {}
options: UseVolatileQueryOptions<Instance<typeof VolatileQuery>> = {},
) {
const queryClient = useContext(Context)! as QueryClient<any>;
const query = useRefQuery(VolatileQuery, queryClient);
Expand Down
2 changes: 1 addition & 1 deletion packages/mst-query/tests/models/ItemQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { api } from '../api/api';
import { ItemModel } from './ItemModel';

export const ItemQuery = createQuery('ItemQuery', {
request: types.model({ id: types.string }),
request: types.model({ id: types.string, id2: types.maybe(types.string) }),
data: types.reference(ItemModel),
async endpoint(args) {
return args.meta.getItem ? args.meta.getItem(args) : api.getItem(args);
Expand Down
44 changes: 44 additions & 0 deletions packages/mst-query/tests/mstQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,50 @@ test('useQuery should run when initialData is given and invalidate is called', a
configureMobx({ enforceActions: 'observed' });
});

test('refetchOnRequestChanged function', async () => {
const { render, q } = setup();

configureMobx({ enforceActions: 'never' });

let id = observable.box('test');
let id2 = observable.box('test2');

const getItem = vi.fn(() => Promise.resolve(itemData));
const testApi = {
...api,
getItem: () => getItem(),
};

const Comp = observer(() => {
useQuery(q.itemQuery, {
request: { id: id.get(), id2: id2.get() },
refetchOnChanged({ prevRequest }) {
return prevRequest.id !== id.get();
},
staleTime: 5000,
meta: { getItem: testApi.getItem },
});
return <div></div>;
});

render(<Comp />);
await wait(0);

expect(getItem).toHaveBeenCalledTimes(1);

id.set('different-test');
await wait(0);

expect(getItem).toHaveBeenCalledTimes(2);

id2.set('different-test2');
await wait(0);

expect(getItem).toHaveBeenCalledTimes(2);

configureMobx({ enforceActions: 'observed' });
});

test('refetchOnMount & refetchOnRequestChanged', async () => {
const { render, q } = setup();

Expand Down