Add declarative api to invalidate queries in mutations (#10200)

In this PR:

1. We added support to declaratively invalidate queries after making a mutation
2. We pull ReactQueryDevtools up in the component tree to make it available during dashboard loading

Also, this PR removes some rules in eslint
This commit is contained in:
Sergei Garin 2024-06-07 12:24:25 +03:00 committed by GitHub
parent 7a20bdc82f
commit 0f7bae3177
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 74 additions and 18 deletions

View File

@ -453,11 +453,6 @@ export default [
selector: ':not(TSModuleDeclaration)[declare=true]',
message: 'No ambient declarations',
},
{
selector: 'ExportDefaultDeclaration:has(Identifier.declaration)',
message:
'Use `export default` on the declaration, instead of as a separate statement',
},
],
// This rule does not work with TypeScript, and TypeScript already does this.
'no-undef': 'off',

View File

@ -88,7 +88,6 @@ import * as object from '#/utilities/object'
import * as authServiceModule from '#/authentication/service'
import type * as types from '../../types/types'
import * as reactQueryDevtools from './ReactQueryDevtools'
// ============================
// === Global configuration ===
@ -173,12 +172,6 @@ export default function App(props: AppProps) {
},
})
const routerFuture: Partial<router.FutureConfig> = {
/* we want to use startTransition to enable concurrent rendering */
/* eslint-disable-next-line @typescript-eslint/naming-convention */
v7_startTransition: true,
}
// Both `BackendProvider` and `InputBindingsProvider` depend on `LocalStorageProvider`.
// Note that the `Router` must be the parent of the `AuthProvider`, because the `AuthProvider`
// will redirect the user between the login/register pages and the dashboard.
@ -193,15 +186,13 @@ export default function App(props: AppProps) {
transition={toastify.Zoom}
limit={3}
/>
<router.BrowserRouter basename={getMainPageUrl().pathname} future={routerFuture}>
<router.BrowserRouter basename={getMainPageUrl().pathname}>
<LocalStorageProvider>
<ModalProvider>
<AppRouter {...props} projectManagerRootDirectory={rootDirectoryPath} />
</ModalProvider>
</LocalStorageProvider>
</router.BrowserRouter>
<reactQueryDevtools.ReactQueryDevtools />
</>
)
}

View File

@ -19,6 +19,8 @@ import LoadingScreen from '#/pages/authentication/LoadingScreen'
import * as errorBoundary from '#/components/ErrorBoundary'
import * as reactQueryDevtools from './ReactQueryDevtools'
// =================
// === Constants ===
// =================
@ -105,6 +107,8 @@ function run(props: Omit<app.AppProps, 'portalRoot'>) {
)}
</React.Suspense>
</errorBoundary.ErrorBoundary>
<reactQueryDevtools.ReactQueryDevtools />
</reactQuery.QueryClientProvider>
</React.StrictMode>
)

View File

@ -34,7 +34,6 @@ export function SetOrganizationNameModal() {
const userPlan =
session && 'user' in session && session.user?.plan != null ? session.user.plan : null
const queryClient = reactQuery.useQueryClient()
const { data: organizationName } = reactQuery.useSuspenseQuery({
queryKey: ['organization', userId],
queryFn: () => {
@ -51,7 +50,7 @@ export function SetOrganizationNameModal() {
const submit = reactQuery.useMutation({
mutationKey: ['organization', userId],
mutationFn: (name: string) => backend.updateOrganization({ name }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['organization', userId] }),
meta: { invalidates: [['organization', userId]], awaitInvalidates: true },
})
const shouldShowModal =

View File

@ -6,13 +6,78 @@
import * as reactQuery from '@tanstack/react-query'
declare module '@tanstack/react-query' {
/**
* Specifies the invalidation behavior of a mutation.
*/
interface Register {
// eslint-disable-next-line no-restricted-syntax
readonly mutationMeta: {
/**
* List of query keys to invalidate when the mutation succeeds.
*/
readonly invalidates?: reactQuery.QueryKey[]
/**
* List of query keys to await invalidation before the mutation is considered successful.
*
* If `true`, all `invalidates` are awaited.
*
* If `false`, no invalidations are awaited.
*
* You can also provide an array of query keys to await.
*
* Queries that are not listed in invalidates will be ignored.
* @default false
*/
readonly awaitInvalidates?: reactQuery.QueryKey[] | boolean
}
}
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const DEFAULT_QUERY_STALE_TIME_MS = 2 * 60 * 1000
/**
* Create a new React Query client.
*/
export function createReactQueryClient() {
return new reactQuery.QueryClient({
const queryClient: reactQuery.QueryClient = new reactQuery.QueryClient({
mutationCache: new reactQuery.MutationCache({
onSuccess: (_data, _variables, _context, mutation) => {
const shouldAwaitInvalidates = mutation.meta?.awaitInvalidates ?? false
const invalidates = mutation.meta?.invalidates ?? []
const invalidatesToAwait = (() => {
if (Array.isArray(shouldAwaitInvalidates)) {
return shouldAwaitInvalidates
} else {
return shouldAwaitInvalidates ? invalidates : []
}
})()
const invalidatesToIgnore = invalidates.filter(
queryKey => !invalidatesToAwait.includes(queryKey)
)
for (const queryKey of invalidatesToIgnore) {
void queryClient.invalidateQueries({
predicate: query => reactQuery.matchQuery({ queryKey }, query),
})
}
if (invalidatesToAwait.length > 0) {
// eslint-disable-next-line no-restricted-syntax
return Promise.all(
invalidatesToAwait.map(queryKey =>
queryClient.invalidateQueries({
predicate: query => reactQuery.matchQuery({ queryKey }, query),
})
)
)
}
},
}),
defaultOptions: {
queries: {
staleTime: DEFAULT_QUERY_STALE_TIME_MS,
retry: (failureCount, error) => {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
const statusesToIgnore = [401, 403, 404]
@ -28,4 +93,6 @@ export function createReactQueryClient() {
},
},
})
return queryClient
}