From 575d27d782b3d63d8e30b84574a8834c674851ee Mon Sep 17 00:00:00 2001 From: Niek Date: Tue, 8 Sep 2020 17:37:21 +0200 Subject: [PATCH] refactor: optimize render path and improve type safety --- src/core/config.ts | 49 ++++++++----- src/core/query.ts | 58 ++++++--------- src/core/queryCache.ts | 70 +++++++++++------- src/core/queryObserver.ts | 55 +++++++------- src/core/types.ts | 15 +++- src/core/utils.ts | 6 +- src/hydration/hydration.ts | 93 +++++++++++------------- src/hydration/tests/hydration.test.tsx | 16 ---- src/react/useBaseQuery.ts | 34 ++++++--- src/react/useDefaultedMutationConfig.tsx | 21 ------ src/react/useDefaultedQueryConfig.tsx | 16 ---- src/react/useInfiniteQuery.ts | 4 +- src/react/useMutation.ts | 18 +++-- src/react/usePaginatedQuery.ts | 4 +- src/react/useQuery.ts | 4 +- 15 files changed, 220 insertions(+), 243 deletions(-) delete mode 100644 src/react/useDefaultedMutationConfig.tsx delete mode 100644 src/react/useDefaultedQueryConfig.tsx diff --git a/src/core/config.ts b/src/core/config.ts index 3fe3841730..e87514462c 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -1,12 +1,14 @@ import { stableStringify } from './utils' -import { +import type { ArrayQueryKey, + MutationConfig, + QueryConfig, QueryKey, QueryKeySerializerFunction, ReactQueryConfig, - QueryConfig, - MutationConfig, + ResolvedQueryConfig, } from './types' +import type { QueryCache } from './queryCache' // TYPES @@ -47,20 +49,19 @@ export const defaultQueryKeySerializerFn: QueryKeySerializerFunction = ( * 2. Defaults from the query cache. * 3. Query/mutation config provided to the query cache method. */ -export const DEFAULT_STALE_TIME = 0 -export const DEFAULT_CACHE_TIME = 5 * 60 * 1000 export const DEFAULT_CONFIG: ReactQueryConfig = { queries: { - cacheTime: DEFAULT_CACHE_TIME, + cacheTime: 5 * 60 * 1000, enabled: true, notifyOnStatusChange: true, + queryFn: () => Promise.reject(), queryKeySerializerFn: defaultQueryKeySerializerFn, refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: true, retry: 3, retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), - staleTime: DEFAULT_STALE_TIME, + staleTime: 0, structuralSharing: true, }, } @@ -85,13 +86,15 @@ export function mergeReactQueryConfigs( } } -export function getDefaultedQueryConfig( - queryCacheConfig?: ReactQueryConfig, +export function getResolvedQueryConfig( + queryCache: QueryCache, + queryKey: QueryKey, contextConfig?: ReactQueryConfig, - config?: QueryConfig, - configOverrides?: QueryConfig -): QueryConfig { - return { + config?: QueryConfig +): ResolvedQueryConfig { + const queryCacheConfig = queryCache.getDefaultConfig() + + const resolvedConfig = { ...DEFAULT_CONFIG.shared, ...DEFAULT_CONFIG.queries, ...queryCacheConfig?.shared, @@ -99,21 +102,28 @@ export function getDefaultedQueryConfig( ...contextConfig?.shared, ...contextConfig?.queries, ...config, - ...configOverrides, - } as QueryConfig + } as ResolvedQueryConfig + + const result = resolvedConfig.queryKeySerializerFn(queryKey) + + resolvedConfig.queryCache = queryCache + resolvedConfig.queryHash = result[0] + resolvedConfig.queryKey = result[1] + + return resolvedConfig } -export function getDefaultedMutationConfig< +export function getResolvedMutationConfig< TResult, TError, TVariables, TSnapshot >( - queryCacheConfig?: ReactQueryConfig, + queryCache: QueryCache, contextConfig?: ReactQueryConfig, - config?: MutationConfig, - configOverrides?: MutationConfig + config?: MutationConfig ): MutationConfig { + const queryCacheConfig = queryCache.getDefaultConfig() return { ...DEFAULT_CONFIG.shared, ...DEFAULT_CONFIG.mutations, @@ -122,6 +132,5 @@ export function getDefaultedMutationConfig< ...contextConfig?.shared, ...contextConfig?.mutations, ...config, - ...configOverrides, } as MutationConfig } diff --git a/src/core/query.ts b/src/core/query.ts index bf59c3593b..54483ac29f 100644 --- a/src/core/query.ts +++ b/src/core/query.ts @@ -10,7 +10,6 @@ import { isOnline, isServer, isValidTimeout, - noop, replaceEqualDeep, sleep, } from './utils' @@ -18,9 +17,9 @@ import { ArrayQueryKey, InitialDataFunction, IsFetchingMoreValue, - QueryConfig, QueryFunction, QueryStatus, + ResolvedQueryConfig, } from './types' import type { QueryCache } from './queryCache' import { QueryObserver, UpdateListener } from './queryObserver' @@ -101,7 +100,7 @@ export type Action = export class Query { queryKey: ArrayQueryKey queryHash: string - config: QueryConfig + config: ResolvedQueryConfig observers: QueryObserver[] state: QueryState cacheTime: number @@ -113,24 +112,20 @@ export class Query { private continueFetch?: () => void private isTransportCancelable?: boolean - constructor( - queryKey: ArrayQueryKey, - queryHash: string, - config: QueryConfig - ) { + constructor(config: ResolvedQueryConfig) { this.config = config - this.queryKey = queryKey - this.queryHash = queryHash - this.queryCache = config.queryCache! + this.queryKey = config.queryKey + this.queryHash = config.queryHash + this.queryCache = config.queryCache + this.cacheTime = config.cacheTime this.observers = [] this.state = getDefaultState(config) - this.cacheTime = config.cacheTime! this.scheduleGc() } - private updateConfig(config: QueryConfig): void { + private updateConfig(config: ResolvedQueryConfig): void { this.config = config - this.cacheTime = Math.max(this.cacheTime, config.cacheTime || 0) + this.cacheTime = Math.max(this.cacheTime, config.cacheTime) } private dispatch(action: Action): void { @@ -247,7 +242,7 @@ export class Query { ) if (staleObserver) { - staleObserver.fetch().catch(noop) + staleObserver.fetch() } // Continue any paused fetch @@ -257,14 +252,8 @@ export class Query { subscribe( listener?: UpdateListener ): QueryObserver { - const observer = new QueryObserver({ - queryCache: this.queryCache, - queryKey: this.queryKey, - ...this.config, - }) - + const observer = new QueryObserver(this.config) observer.subscribe(listener) - return observer } @@ -291,7 +280,7 @@ export class Query { async refetch( options?: RefetchOptions, - config?: QueryConfig + config?: ResolvedQueryConfig ): Promise { try { return await this.fetch(undefined, config) @@ -305,7 +294,7 @@ export class Query { async fetchMore( fetchMoreVariable?: unknown, options?: FetchMoreOptions, - config?: QueryConfig + config?: ResolvedQueryConfig ): Promise { return this.fetch( { @@ -320,7 +309,7 @@ export class Query { async fetch( options?: FetchOptions, - config?: QueryConfig + config?: ResolvedQueryConfig ): Promise { // If we are already fetching, return current promise if (this.promise) { @@ -334,11 +323,6 @@ export class Query { config = this.config - // Check if there is a query function - if (typeof config.queryFn !== 'function') { - return - } - // Get the query function params const filter = config.queryFnParamsFilter const params = filter ? filter(this.queryKey) : this.queryKey @@ -385,12 +369,12 @@ export class Query { } private async startFetch( - config: QueryConfig, + config: ResolvedQueryConfig, params: unknown[], _options?: FetchOptions ): Promise { // Create function to fetch the data - const fetchData = () => config.queryFn!(...params) + const fetchData = () => config.queryFn(...params) // Set to fetching state if not already in it if (!this.state.isFetching) { @@ -402,7 +386,7 @@ export class Query { } private async startInfiniteFetch( - config: QueryConfig, + config: ResolvedQueryConfig, params: unknown[], options?: FetchOptions ): Promise { @@ -427,7 +411,7 @@ export class Query { cursor = config.getFetchMore(lastPage, pages) } - const page = await config.queryFn!(...params, cursor) + const page = await config.queryFn(...params, cursor) return prepend ? [page, ...pages] : [...pages, page] } @@ -457,7 +441,7 @@ export class Query { } private async tryFetchData( - config: QueryConfig, + config: ResolvedQueryConfig, fn: QueryFunction ): Promise { return new Promise((outerResolve, outerReject) => { @@ -567,7 +551,7 @@ function getLastPage(pages: TResult[], previous?: boolean): TResult { } function hasMorePages( - config: QueryConfig, + config: ResolvedQueryConfig, pages: unknown, previous?: boolean ): boolean | undefined { @@ -577,7 +561,7 @@ function hasMorePages( } function getDefaultState( - config: QueryConfig + config: ResolvedQueryConfig ): QueryState { const initialData = typeof config.initialData === 'function' diff --git a/src/core/queryCache.ts b/src/core/queryCache.ts index 31585a5da1..ed932800b2 100644 --- a/src/core/queryCache.ts +++ b/src/core/queryCache.ts @@ -8,7 +8,7 @@ import { isOnline, isServer, } from './utils' -import { getDefaultedQueryConfig } from './config' +import { getResolvedQueryConfig } from './config' import { Query } from './query' import { QueryConfig, @@ -17,6 +17,7 @@ import { ReactQueryConfig, TypedQueryFunction, TypedQueryFunctionArgs, + ResolvedQueryConfig, } from './types' // TYPES @@ -98,12 +99,11 @@ export class QueryCache { return this.config.defaultConfig } - getDefaultedQueryConfig( + getResolvedQueryConfig( + queryKey: QueryKey, config?: QueryConfig - ): QueryConfig { - return getDefaultedQueryConfig(this.getDefaultConfig(), undefined, config, { - queryCache: this, - }) + ): ResolvedQueryConfig { + return getResolvedQueryConfig(this, queryKey, undefined, config) } subscribe(listener: QueryCacheListener): () => void { @@ -133,13 +133,12 @@ export class QueryCache { if (typeof predicate === 'function') { predicateFn = predicate as QueryPredicateFn } else { - const config = this.getDefaultedQueryConfig() - const [queryHash, queryKey] = config.queryKeySerializerFn!(predicate) + const resolvedConfig = this.getResolvedQueryConfig(predicate) predicateFn = d => options?.exact - ? d.queryHash === queryHash - : deepIncludes(d.queryKey, queryKey) + ? d.queryHash === resolvedConfig.queryHash + : deepIncludes(d.queryKey, resolvedConfig.queryKey) } return this.queriesArray.filter(predicateFn) @@ -151,6 +150,12 @@ export class QueryCache { return this.getQueries(predicate, { exact: true })[0] } + getQueryByHash( + queryHash: string + ): Query | undefined { + return this.queries[queryHash] + } + getQueryData(predicate: QueryPredicate): TResult | undefined { return this.getQuery(predicate)?.state.data } @@ -215,21 +220,27 @@ export class QueryCache { } buildQuery( - userQueryKey: QueryKey, - queryConfig?: QueryConfig + queryKey: QueryKey, + config?: QueryConfig ): Query { - const config = this.getDefaultedQueryConfig(queryConfig) - const [queryHash, queryKey] = config.queryKeySerializerFn!(userQueryKey) + const resolvedConfig = this.getResolvedQueryConfig(queryKey, config) + let query = this.getQueryByHash(resolvedConfig.queryHash) - if (this.queries[queryHash]) { - return this.queries[queryHash] as Query + if (!query) { + query = this.createQuery(resolvedConfig) } - const query = new Query(queryKey, queryHash, config) + return query + } + + createQuery( + config: ResolvedQueryConfig + ): Query { + const query = new Query(config) // A frozen cache does not add new queries to the cache if (!this.config.frozen) { - this.queries[queryHash] = query + this.queries[query.queryHash] = query this.queriesArray.push(query) this.notifyGlobalListeners(query) } @@ -296,19 +307,21 @@ export class QueryCache { PrefetchQueryOptions | undefined >(args) - // https://github.com/tannerlinsley/react-query/issues/652 - const configWithoutRetry = this.getDefaultedQueryConfig({ + const resolvedConfig = this.getResolvedQueryConfig(queryKey, { + // https://github.com/tannerlinsley/react-query/issues/652 retry: false, ...config, }) + let query = this.getQueryByHash(resolvedConfig.queryHash) + + if (!query) { + query = this.createQuery(resolvedConfig) + } + try { - const query = this.buildQuery( - queryKey, - configWithoutRetry - ) if (options?.force || query.isStaleByTime(config.staleTime)) { - await query.fetch(undefined, configWithoutRetry) + await query.fetch(undefined, resolvedConfig) } return query.state.data } catch (error) { @@ -323,17 +336,18 @@ export class QueryCache { updater: Updater, config?: QueryConfig ) { - const query = this.getQuery(queryKey) + const resolvedConfig = this.getResolvedQueryConfig(queryKey, config) + const query = this.getQueryByHash(resolvedConfig.queryHash) if (query) { query.setData(updater) return } - this.buildQuery(queryKey, { + this.createQuery({ initialFetched: true, initialData: functionalUpdate(updater, undefined), - ...config, + ...resolvedConfig, }) } } diff --git a/src/core/queryObserver.ts b/src/core/queryObserver.ts index 517bf8ea9f..0b7cc76f97 100644 --- a/src/core/queryObserver.ts +++ b/src/core/queryObserver.ts @@ -4,30 +4,26 @@ import { isDocumentVisible, isValidTimeout, } from './utils' -import type { QueryResult, QueryObserverConfig } from './types' +import type { QueryResult, ResolvedQueryConfig } from './types' import type { Query, Action, FetchMoreOptions, RefetchOptions } from './query' -import type { QueryCache } from './queryCache' export type UpdateListener = ( result: QueryResult ) => void export class QueryObserver { - config: QueryObserverConfig + config: ResolvedQueryConfig - private queryCache: QueryCache private currentQuery!: Query private currentResult!: QueryResult private previousQueryResult?: QueryResult - private updateListener?: UpdateListener + private listener?: UpdateListener private initialFetchedCount: number private staleTimeoutId?: number private refetchIntervalId?: number - private started?: boolean - constructor(config: QueryObserverConfig) { + constructor(config: ResolvedQueryConfig) { this.config = config - this.queryCache = config.queryCache! this.initialFetchedCount = 0 // Bind exposed methods @@ -40,8 +36,7 @@ export class QueryObserver { } subscribe(listener?: UpdateListener): () => void { - this.started = true - this.updateListener = listener + this.listener = listener this.currentQuery.subscribeObserver(this) if (this.config.enabled && this.config.forceFetchOnMount) { @@ -55,25 +50,25 @@ export class QueryObserver { } unsubscribe(): void { - this.started = false - this.updateListener = undefined + this.listener = undefined this.clearTimers() this.currentQuery.unsubscribeObserver(this) } - updateConfig(config: QueryObserverConfig): void { + updateConfig(config: ResolvedQueryConfig): void { const prevConfig = this.config - this.config = config + const prevQuery = this.currentQuery - const updated = this.updateQuery() + this.config = config + this.updateQuery() - // Take no further actions if the observer did not start yet - if (!this.started) { + // Take no further actions if there is no subscriber + if (!this.listener) { return } // If we subscribed to a new query, optionally fetch and update refetch - if (updated) { + if (this.currentQuery !== prevQuery) { this.optionalFetch() this.updateTimers() return @@ -144,7 +139,7 @@ export class QueryObserver { } private notify(): void { - this.updateListener?.(this.currentResult) + this.listener?.(this.currentResult) } private updateStaleTimeout(): void { @@ -265,7 +260,7 @@ export class QueryObserver { } } - private updateQuery(): boolean { + private updateQuery(): void { const prevQuery = this.currentQuery // Remove the initial data when there is an existing query @@ -275,23 +270,27 @@ export class QueryObserver { ? { ...this.config, initialData: undefined } : this.config - const newQuery = this.queryCache.buildQuery(config.queryKey, config) + let query = config.queryCache.getQueryByHash( + config.queryHash + ) - if (newQuery === prevQuery) { - return false + if (!query) { + query = config.queryCache.createQuery(config) + } + + if (query === prevQuery) { + return } this.previousQueryResult = this.currentResult - this.currentQuery = newQuery - this.initialFetchedCount = newQuery.state.fetchedCount + this.currentQuery = query + this.initialFetchedCount = query.state.fetchedCount this.updateResult() - if (this.started) { + if (this.listener) { prevQuery?.unsubscribeObserver(this) this.currentQuery.subscribeObserver(this) } - - return true } onQueryUpdate(action: Action): void { diff --git a/src/core/types.ts b/src/core/types.ts index 06339cc888..eff7d0fd9e 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -149,10 +149,6 @@ export interface QueryObserverConfig< * Defaults to `false`. */ keepPreviousData?: boolean - /** - * By default the query cache from the context is used, but a different cache can be specified. - */ - queryCache?: QueryCache } export interface QueryConfig @@ -164,6 +160,17 @@ export interface PaginatedQueryConfig export interface InfiniteQueryConfig extends QueryObserverConfig {} +export interface ResolvedQueryConfig + extends QueryConfig { + cacheTime: number + queryCache: QueryCache + queryFn: QueryFunction + queryHash: string + queryKey: ArrayQueryKey + queryKeySerializerFn: QueryKeySerializerFunction + staleTime: number +} + export type IsFetchingMoreValue = 'previous' | 'next' | false export enum QueryStatus { diff --git a/src/core/utils.ts b/src/core/utils.ts index ca67602057..a1a36a3e02 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -25,7 +25,9 @@ export class CancelledError {} // UTILS let _uid = 0 -export const uid = () => _uid++ +export function uid(): number { + return _uid++ +} export const isServer = typeof window === 'undefined' @@ -129,7 +131,7 @@ export function getQueryArgs( options = args[3] } - config = config ? { queryKey, ...config } : { queryKey } + config = config || {} if (queryFn) { config = { ...config, queryFn } diff --git a/src/hydration/hydration.ts b/src/hydration/hydration.ts index d8c709213c..1679dcd85a 100644 --- a/src/hydration/hydration.ts +++ b/src/hydration/hydration.ts @@ -1,6 +1,4 @@ -import { DEFAULT_CACHE_TIME } from '../core/config' - -import type { Query, QueryCache, QueryKey, QueryConfig } from 'react-query' +import type { Query, QueryCache, QueryKey } from 'react-query' export interface DehydratedQueryConfig { cacheTime?: number @@ -20,57 +18,52 @@ export interface DehydratedState { export type ShouldDehydrateFunction = ( query: Query ) => boolean + export interface DehydrateConfig { shouldDehydrate?: ShouldDehydrateFunction } +// Most config is not dehydrated but instead meant to configure again when +// consuming the de/rehydrated data, typically with useQuery on the client. +// Sometimes it might make sense to prefetch data on the server and include +// in the html-payload, but not consume it on the initial render. function dehydrateQuery( query: Query ): DehydratedQuery { - const dehydratedQuery: DehydratedQuery = { - config: {}, + return { + config: { + cacheTime: query.cacheTime, + }, + data: query.state.data, queryKey: query.queryKey, updatedAt: query.state.updatedAt, } - - // Most config is not dehydrated but instead meant to configure again when - // consuming the de/rehydrated data, typically with useQuery on the client. - // Sometimes it might make sense to prefetch data on the server and include - // in the html-payload, but not consume it on the initial render. - // We still schedule stale and garbage collection right away, which means - // we need to specifically include staleTime and cacheTime in dehydration. - if (query.cacheTime !== DEFAULT_CACHE_TIME) { - dehydratedQuery.config.cacheTime = query.cacheTime - } - if (query.state.data !== undefined) { - dehydratedQuery.data = query.state.data - } - - return dehydratedQuery } -const defaultShouldDehydrate: ShouldDehydrateFunction = query => - query.state.status === 'success' +function defaultShouldDehydrate( + query: Query +) { + return query.state.status === 'success' +} export function dehydrate( queryCache: QueryCache, dehydrateConfig?: DehydrateConfig ): DehydratedState { const config = dehydrateConfig || {} - const { shouldDehydrate = defaultShouldDehydrate } = config - const dehydratedState: DehydratedState = { - queries: [], - } - for (const query of queryCache.getQueries()) { + const shouldDehydrate = config.shouldDehydrate || defaultShouldDehydrate + const queries: DehydratedQuery[] = [] + + queryCache.getQueries().forEach(query => { if (shouldDehydrate(query)) { - dehydratedState.queries.push(dehydrateQuery(query)) + queries.push(dehydrateQuery(query)) } - } + }) - return dehydratedState + return { queries } } -export function hydrate( +export function hydrate( queryCache: QueryCache, dehydratedState: unknown ): void { @@ -80,23 +73,25 @@ export function hydrate( const queries = (dehydratedState as DehydratedState).queries || [] - for (const dehydratedQuery of queries) { - const queryKey = dehydratedQuery.queryKey - const queryConfig = dehydratedQuery.config as QueryConfig - - let query = queryCache.getQuery(queryKey) - - if (query) { - if (query.state.updatedAt < dehydratedQuery.updatedAt) { - query.setData(dehydratedQuery.data as TResult, { - updatedAt: dehydratedQuery.updatedAt, - }) - } - } else { - query = queryCache.buildQuery(queryKey, queryConfig) - query.setData(dehydratedQuery.data as TResult, { - updatedAt: dehydratedQuery.updatedAt, - }) + queries.forEach(dehydratedQuery => { + const resolvedConfig = queryCache.getResolvedQueryConfig( + dehydratedQuery.queryKey, + dehydratedQuery.config + ) + + let query = queryCache.getQueryByHash(resolvedConfig.queryHash) + + // Do not hydrate if an existing query exists with newer data + if (query && query.state.updatedAt >= dehydratedQuery.updatedAt) { + return } - } + + if (!query) { + query = queryCache.createQuery(resolvedConfig) + } + + query.setData(dehydratedQuery.data, { + updatedAt: dehydratedQuery.updatedAt, + }) + }) } diff --git a/src/hydration/tests/hydration.test.tsx b/src/hydration/tests/hydration.test.tsx index 2ba9d49631..e30b1862a6 100644 --- a/src/hydration/tests/hydration.test.tsx +++ b/src/hydration/tests/hydration.test.tsx @@ -124,22 +124,6 @@ describe('dehydration and rehydration', () => { hydrationQueryCache.clear({ notify: false }) }) - test('should not include default config in dehydration', async () => { - const queryCache = makeQueryCache() - await queryCache.prefetchQuery('string', () => fetchData('string')) - const dehydrated = dehydrate(queryCache) - - // This is testing implementation details that can change and are not - // part of the public API, but is important for keeping the payload small - // Exact shape is not important here, just that staleTime and cacheTime - // (and any future other config) is not included in it - const dehydratedQuery = dehydrated?.queries.find( - query => (query?.queryKey as Array)[0] === 'string' - ) - expect(dehydratedQuery).toBeTruthy() - expect(dehydratedQuery?.config.cacheTime).toBe(undefined) - }) - test('should only hydrate successful queries by default', async () => { const consoleMock = jest.spyOn(console, 'error') consoleMock.mockImplementation(() => undefined) diff --git a/src/react/useBaseQuery.ts b/src/react/useBaseQuery.ts index 43023f2665..6509f8efd0 100644 --- a/src/react/useBaseQuery.ts +++ b/src/react/useBaseQuery.ts @@ -1,22 +1,32 @@ import React from 'react' import { useRerenderer } from './utils' +import { getResolvedQueryConfig } from '../core/config' import { QueryObserver } from '../core/queryObserver' -import { QueryResultBase, QueryObserverConfig } from '../core/types' -import { useDefaultedQueryConfig } from './useDefaultedQueryConfig' +import { QueryResultBase, QueryConfig, QueryKey } from '../core/types' +import { useQueryCache } from './ReactQueryCacheProvider' +import { useContextConfig } from './ReactQueryConfigProvider' export function useBaseQuery( - config: QueryObserverConfig = {} + queryKey: QueryKey, + config?: QueryConfig ): QueryResultBase { - config = useDefaultedQueryConfig(config) - - // Make a rerender function const rerender = useRerenderer() + const cache = useQueryCache() + const contextConfig = useContextConfig() + + // Get resolved config + const resolvedConfig = getResolvedQueryConfig( + cache, + queryKey, + contextConfig, + config + ) // Create query observer const observerRef = React.useRef>() const firstRender = !observerRef.current - const observer = observerRef.current || new QueryObserver(config) + const observer = observerRef.current || new QueryObserver(resolvedConfig) observerRef.current = observer // Subscribe to the observer @@ -30,20 +40,24 @@ export function useBaseQuery( // Update config if (!firstRender) { - observer.updateConfig(config) + observer.updateConfig(resolvedConfig) } const result = observer.getCurrentResult() // Handle suspense - if (config.suspense || config.useErrorBoundary) { + if (resolvedConfig.suspense || resolvedConfig.useErrorBoundary) { const query = observer.getCurrentQuery() if (result.isError && query.state.throwInErrorBoundary) { throw result.error } - if (config.enabled && config.suspense && !result.isSuccess) { + if ( + resolvedConfig.enabled && + resolvedConfig.suspense && + !result.isSuccess + ) { const unsubscribe = observer.subscribe() throw observer.fetch().finally(unsubscribe) } diff --git a/src/react/useDefaultedMutationConfig.tsx b/src/react/useDefaultedMutationConfig.tsx deleted file mode 100644 index c429b9489c..0000000000 --- a/src/react/useDefaultedMutationConfig.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { MutationConfig } from '../core/types' -import { getDefaultedMutationConfig } from '../core/config' -import { useQueryCache } from './ReactQueryCacheProvider' -import { useContextConfig } from './ReactQueryConfigProvider' - -export function useDefaultedMutationConfig< - TResult, - TError, - TVariables, - TSnapshot ->( - config?: MutationConfig -): MutationConfig { - const contextConfig = useContextConfig() - const contextQueryCache = useQueryCache() - const queryCache = config?.queryCache || contextQueryCache - const queryCacheConfig = queryCache.getDefaultConfig() - return getDefaultedMutationConfig(queryCacheConfig, contextConfig, config, { - queryCache, - }) -} diff --git a/src/react/useDefaultedQueryConfig.tsx b/src/react/useDefaultedQueryConfig.tsx deleted file mode 100644 index 44453b2b70..0000000000 --- a/src/react/useDefaultedQueryConfig.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { QueryConfig } from '../core/types' -import { getDefaultedQueryConfig } from '../core/config' -import { useQueryCache } from './ReactQueryCacheProvider' -import { useContextConfig } from './ReactQueryConfigProvider' - -export function useDefaultedQueryConfig( - config?: QueryConfig -): QueryConfig { - const contextConfig = useContextConfig() - const contextQueryCache = useQueryCache() - const queryCache = config?.queryCache || contextQueryCache - const queryCacheConfig = queryCache.getDefaultConfig() - return getDefaultedQueryConfig(queryCacheConfig, contextConfig, config, { - queryCache, - }) -} diff --git a/src/react/useInfiniteQuery.ts b/src/react/useInfiniteQuery.ts index 1e30b48bd4..799e2236bd 100644 --- a/src/react/useInfiniteQuery.ts +++ b/src/react/useInfiniteQuery.ts @@ -51,6 +51,6 @@ export function useInfiniteQuery( export function useInfiniteQuery( ...args: any[] ): InfiniteQueryResult { - const config = getQueryArgs(args)[1] - return useBaseQuery({ ...config, infinite: true }) + const [queryKey, config] = getQueryArgs(args) + return useBaseQuery(queryKey, { ...config, infinite: true }) } diff --git a/src/react/useMutation.ts b/src/react/useMutation.ts index 7826a70051..2d0a313101 100644 --- a/src/react/useMutation.ts +++ b/src/react/useMutation.ts @@ -1,7 +1,7 @@ import React from 'react' -import { useDefaultedMutationConfig } from './useDefaultedMutationConfig' import { useMountedCallback } from './utils' +import { getResolvedMutationConfig } from '../core/config' import { Console, uid, getStatusProps } from '../core/utils' import { QueryStatus, @@ -11,6 +11,8 @@ import { MutateConfig, MutationResult, } from '../core/types' +import { useQueryCache } from './ReactQueryCacheProvider' +import { useContextConfig } from './ReactQueryConfigProvider' // TYPES @@ -59,7 +61,7 @@ type Action = // HOOK -function getDefaultState(): State { +function getDefaultState(): State { return { ...getStatusProps(QueryStatus.Idle), data: undefined, @@ -106,7 +108,11 @@ export function useMutation< mutationFn: MutationFunction, config: MutationConfig = {} ): MutationResultPair { - config = useDefaultedMutationConfig(config) + const cache = useQueryCache() + const contextConfig = useContextConfig() + + // Get resolved config + const resolvedConfig = getResolvedMutationConfig(cache, contextConfig, config) const [state, unsafeDispatch] = React.useReducer( mutationReducer as Reducer, Action>, @@ -118,8 +124,8 @@ export function useMutation< const latestMutationRef = React.useRef() const latestMutationFnRef = React.useRef(mutationFn) latestMutationFnRef.current = mutationFn - const latestConfigRef = React.useRef(config) - latestConfigRef.current = config + const latestConfigRef = React.useRef(resolvedConfig) + latestConfigRef.current = resolvedConfig const mutate = React.useCallback( async ( @@ -184,7 +190,7 @@ export function useMutation< React.useEffect(() => { const latestConfig = latestConfigRef.current const { suspense, useErrorBoundary } = latestConfig - if ((useErrorBoundary ?? suspense) && state.error) { + if ((useErrorBoundary || suspense) && state.error) { throw state.error } }, [state.error]) diff --git a/src/react/usePaginatedQuery.ts b/src/react/usePaginatedQuery.ts index 4eefce3e9b..0ee71c8f9d 100644 --- a/src/react/usePaginatedQuery.ts +++ b/src/react/usePaginatedQuery.ts @@ -56,8 +56,8 @@ export function usePaginatedQuery( export function usePaginatedQuery( ...args: any[] ): PaginatedQueryResult { - const config = getQueryArgs(args)[1] - const result = useBaseQuery({ + const [queryKey, config] = getQueryArgs(args) + const result = useBaseQuery(queryKey, { keepPreviousData: true, ...config, }) diff --git a/src/react/useQuery.ts b/src/react/useQuery.ts index ec7a915082..8e0fbee44b 100644 --- a/src/react/useQuery.ts +++ b/src/react/useQuery.ts @@ -47,6 +47,6 @@ export function useQuery( export function useQuery( ...args: any[] ): QueryResult { - const config = getQueryArgs(args)[1] - return useBaseQuery(config) + const [queryKey, config] = getQueryArgs(args) + return useBaseQuery(queryKey, config) }