Skip to content

Commit 6536e86

Browse files
committed
refactor(orval): move snapshot handling to global defaults
1 parent c329b5a commit 6536e86

File tree

9 files changed

+64
-62
lines changed

9 files changed

+64
-62
lines changed

jsapp/js/account/organization/MemberRemoveModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import subscriptionStore from '#/account/subscriptionStore'
2+
import { queryClient } from '#/api/queryClient'
23
import {
34
getOrganizationsMembersDestroyMutationOptions,
45
useOrganizationsMembersDestroy,
@@ -11,7 +12,6 @@ import KoboModalContent from '#/components/modals/koboModalContent'
1112
import KoboModalFooter from '#/components/modals/koboModalFooter'
1213
import KoboModalHeader from '#/components/modals/koboModalHeader'
1314
import envStore from '#/envStore'
14-
import { queryClient } from '#/query/queryClient'
1515
import { getSimpleMMOLabel } from './organization.utils'
1616

1717
interface MemberRemoveModalProps {

jsapp/js/account/organization/OrganizationSettingsRoute.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import styles from '#/account/organization/organizationSettingsRoute.module.scss
66
import subscriptionStore from '#/account/subscriptionStore'
77
import { MemberRoleEnum } from '#/api/models/memberRoleEnum'
88
import type { OrganizationTypeEnum } from '#/api/models/organizationTypeEnum'
9+
import { queryClient } from '#/api/queryClient'
910
import {
1011
getOrganizationsRetrieveQueryKey,
1112
useOrganizationsPartialUpdate,
@@ -17,7 +18,6 @@ import LoadingSpinner from '#/components/common/loadingSpinner'
1718
import TextBox from '#/components/common/textBox'
1819
import envStore from '#/envStore'
1920
import useWhenStripeIsEnabled from '#/hooks/useWhenStripeIsEnabled.hook'
20-
import { queryClient } from '#/query/queryClient'
2121
import { getSimpleMMOLabel } from './organization.utils'
2222

2323
export const ORGANIZATION_TYPES: { [P in OrganizationTypeEnum]: { name: P; label: string } } = {

jsapp/js/api/mutation-defaults/common.ts

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Updater } from '@tanstack/react-query'
2-
import { queryClient } from '#/query/queryClient'
2+
import { queryClient } from '../queryClient'
33

44
/**
55
* Beware that `getUsersListQueryKey(undefined)` (and alike) doesn't select all pages for list endpoint as expected!
@@ -38,43 +38,13 @@ export const invalidateItem = (queryKey: readonly unknown[]) => {
3838
queryClient.invalidateQueries({ queryKey })
3939
}
4040

41-
interface CommonContext {
42-
snapshots?: ReadonlyArray<readonly [ReadonlyArray<unknown>, unknown]>
43-
}
44-
/**
45-
* After an optimistic update roll it back if server responds with an error.
46-
*
47-
* Note that in rare case when client receives error despite the request suceeding (e.g. untimely loss of connection),
48-
* then it will be reconciled as part of {@link onSettledInvalidateSnapshots}.
49-
*
50-
* To be used together with {@link optimisticallyUpdateList} and {@link optimisticallyUpdateItem}.
51-
*/
52-
export const onErrorRestoreSnapshots = (_error: unknown, _variables: unknown, context?: CommonContext): void => {
53-
for (const [snapshotKey, snapshotData] of context?.snapshots ?? [])
54-
queryClient.setQueryData(snapshotKey, snapshotData)
55-
}
56-
/**
57-
* After an optimistic update (rolled back or not), invalidate and thus re-fetch data from server in background.
58-
* - If server response will match current cache, then not even a re-render will happen. Better be safe and confirm.
59-
* - If server response will NOT match current cache, then it will update cache and re-render accordingly.
60-
*
61-
* To be used together with {@link optimisticallyUpdateList} and {@link optimisticallyUpdateItem}.
62-
*/
63-
export const onSettledInvalidateSnapshots = (
64-
_data: unknown,
65-
_error: unknown,
66-
_variables: unknown,
67-
context?: CommonContext,
68-
): void => {
69-
for (const [snapshotKey] of context?.snapshots ?? []) queryClient.invalidateQueries({ queryKey: snapshotKey })
70-
}
7141
/**
7242
* Optimistically apply `updater` to all pages of the `queryKey` list in cache.
7343
*
7444
* Handles selecting and iterating over pages of list (and not items, see more at {@link filterListSnapshots}),
7545
* and cancels in-flight queries that may race-condition to overwrite the optimistic update.
7646
*
77-
* Returns snapshots, to be consumed by {@link onErrorRestoreSnapshots} and {@link onSettledInvalidateSnapshots}.
47+
* Returns snapshots, see global defaults {@link onErrorRestoreSnapshots} and {@link onSettledInvalidateSnapshots}.
7848
*/
7949
export const optimisticallyUpdateList = async <T>(
8050
queryKey: readonly unknown[],
@@ -97,7 +67,7 @@ export const optimisticallyUpdateList = async <T>(
9767
*
9868
* Also cancels in-flight queries that may race-condition to overwrite the optimistic update.
9969
*
100-
* Returns a snapshot, to be consumed by {@link onErrorRestoreSnapshots} and {@link onSettledInvalidateSnapshots}.
70+
* Returns a snapshot, see global defaults {@link onErrorRestoreSnapshots} and {@link onSettledInvalidateSnapshots}.
10171
*/
10272
export const optimisticallyUpdateItem = async <T>(
10373
queryKey: readonly unknown[],

jsapp/js/api/mutation-defaults/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './user-team-organization-usage'
66
/**
77
* Write at least invalidation for every mutation in use.
88
* Optimistic updates are optional, requires different UX approach and often is just an overkill.
9+
* See when NOT to use optimistic update: https://tkdodo.eu/blog/mastering-mutations-in-react-query#optimistic-updates
910
*
1011
* Generally, when writing invalidation only:
1112
* - onSettled: invalidate affected queries

jsapp/js/api/mutation-defaults/user-team-organization-usage.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,10 @@ import {
1616
type organizationsMembersListResponse,
1717
type organizationsMembersRetrieveResponse,
1818
} from '#/api/react-query/user-team-organization-usage'
19-
import { queryClient } from '#/query/queryClient'
2019
import session from '#/stores/session'
2120
import { getAssetUIDFromUrl } from '#/utils'
22-
import {
23-
invalidateItem,
24-
invalidateList,
25-
onErrorRestoreSnapshots,
26-
onSettledInvalidateSnapshots,
27-
optimisticallyUpdateItem,
28-
optimisticallyUpdateList,
29-
} from './common'
21+
import { queryClient } from '../queryClient'
22+
import { invalidateItem, invalidateList, optimisticallyUpdateItem, optimisticallyUpdateList } from './common'
3023

3124
queryClient.setMutationDefaults(
3225
getOrganizationsPartialUpdateMutationOptions().mutationKey!,
@@ -52,6 +45,10 @@ queryClient.setMutationDefaults(
5245
* Note that:
5346
* - when creating an item then no need to invalidate any of existing items.
5447
* - when creating an item then invalidate member list as well, because KPI placeholds members based on invites.
48+
*
49+
* Also a good example when NOT to use optimistic updates, because:
50+
* - in current UI it would gain nothing to insert the single item cache
51+
* - it's impossible to guess the order of items in the lists, so don't even try
5552
*/
5653
mutation: {
5754
onSettled: (_data, _error, variables) => {
@@ -94,8 +91,6 @@ queryClient.setMutationDefaults(
9491
snapshots: [...listSnapshots, itemSnapshot],
9592
}
9693
},
97-
onError: onErrorRestoreSnapshots,
98-
onSettled: onSettledInvalidateSnapshots,
9994
},
10095
}),
10196
)
@@ -189,8 +184,6 @@ queryClient.setMutationDefaults(
189184
snapshots: [...invitesSnapshots, ...membersSnapshots, inviteSnapshot, memberSnapshot],
190185
}
191186
},
192-
onError: onErrorRestoreSnapshots,
193-
onSettled: onSettledInvalidateSnapshots,
194187
},
195188
}),
196189
)
@@ -232,8 +225,6 @@ queryClient.setMutationDefaults(
232225
// If user is removing themselves, we need to clear the session
233226
if (username === session.currentAccount?.username) session.refreshAccount()
234227
},
235-
onError: onErrorRestoreSnapshots,
236-
onSettled: onSettledInvalidateSnapshots,
237228
},
238229
}),
239230
)
@@ -282,8 +273,6 @@ queryClient.setMutationDefaults(
282273
snapshots: [...listSnapshots, memberSnapshot],
283274
}
284275
},
285-
onError: onErrorRestoreSnapshots,
286-
onSettled: onSettledInvalidateSnapshots,
287276
},
288277
}),
289278
)

jsapp/js/api/queryClient.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { QueryClient } from '@tanstack/react-query'
2+
3+
interface CommonContext {
4+
snapshots?: ReadonlyArray<readonly [ReadonlyArray<unknown>, unknown]>
5+
}
6+
/**
7+
* After an optimistic update roll it back if server responds with an error.
8+
*
9+
* Note that in rare case when client receives error despite the request suceeding (e.g. untimely loss of connection),
10+
* then it will be reconciled as part of {@link onSettledInvalidateSnapshots}.
11+
*
12+
* To be used together with {@link optimisticallyUpdateList} and {@link optimisticallyUpdateItem}.
13+
*/
14+
const onErrorRestoreSnapshots = (_error: unknown, _variables: unknown, context?: unknown): void => {
15+
for (const [snapshotKey, snapshotData] of (context as CommonContext)?.snapshots ?? [])
16+
queryClient.setQueryData(snapshotKey, snapshotData)
17+
}
18+
/**
19+
* After an optimistic update (rolled back or not), invalidate and thus re-fetch data from server in background.
20+
* - If server response will match current cache, then not even a re-render will happen. Better be safe and confirm.
21+
* - If server response will NOT match current cache, then it will update cache and re-render accordingly.
22+
*
23+
* Note: this helper doesn't handle [concurrent updates](https://tkdodo.eu/blog/concurrent-optimistic-updates-in-react-query),
24+
* but it's a fine trade-off to keep it simple at cost of not handling few rare flickers.
25+
*
26+
* To be used together with {@link optimisticallyUpdateList} and {@link optimisticallyUpdateItem}.
27+
*/
28+
const onSettledInvalidateSnapshots = (
29+
_data: unknown,
30+
_error: unknown,
31+
_variables: unknown,
32+
context?: unknown,
33+
): void => {
34+
for (const [snapshotKey] of (context as CommonContext)?.snapshots ?? [])
35+
queryClient.invalidateQueries({ queryKey: snapshotKey })
36+
}
37+
38+
// Some shared defaults and config can be set here!
39+
// Docs: https://tanstack.com/query/v5/docs/reference/QueryClient#queryclient
40+
// See: https://tanstack.com/query/v5/docs/framework/react/guides/important-defaults
41+
const queryClient = new QueryClient({
42+
defaultOptions: {
43+
mutations: {
44+
onError: onErrorRestoreSnapshots,
45+
onSettled: onSettledInvalidateSnapshots,
46+
}
47+
},
48+
})
49+
50+
export { queryClient }

jsapp/js/app.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import DocumentTitle from 'react-document-title'
1212
import reactMixin from 'react-mixin'
1313
import { Outlet } from 'react-router-dom'
1414
import Reflux from 'reflux'
15+
import { queryClient } from '#/api/queryClient'
1516
import bem from '#/bem'
1617
import BigModal from '#/components/bigModal/bigModal'
1718
import Drawer from '#/components/drawer'
@@ -25,7 +26,6 @@ import { RootContextProvider } from '#/rootContextProvider.component'
2526
import InvalidatedPassword from '#/router/invalidatedPassword.component'
2627
import { isInvalidatedPasswordRouteBlockerActive, isTOSAgreementRouteBlockerActive } from '#/router/routerUtils'
2728
import TOSAgreement from '#/router/tosAgreement.component'
28-
import { queryClient } from './query/queryClient.ts'
2929
import { router, routerGetAssetId, withRouter } from './router/legacy'
3030
import { Tracking } from './router/useTracking'
3131
import { themeKobo } from './theme'

jsapp/js/query/queryClient.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

jsapp/js/router/basicLayout.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import React from 'react'
33
import { MantineProvider } from '@mantine/core'
44
import { QueryClientProvider } from '@tanstack/react-query'
55
import DocumentTitle from 'react-document-title'
6+
import { queryClient } from '#/api/queryClient'
67
import bem from '#/bem'
78
import AccountMenu from '#/components/header/accountMenu'
89
import MainHeaderBase from '#/components/header/mainHeaderBase.component'
910
import MainHeaderLogo from '#/components/header/mainHeaderLogo.component'
1011
import sessionStore from '#/stores/session'
1112
import { themeKobo } from '#/theme'
12-
import { queryClient } from '../query/queryClient'
1313
import ToasterConfig from '../toasterConfig'
1414
import { RequireOrg } from './RequireOrg'
1515
import { Tracking } from './useTracking'

0 commit comments

Comments
 (0)