From f32bbf0a75a0e115dd61db81a2401525097f3560 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 12 Feb 2025 23:53:25 -0700 Subject: [PATCH] update to use new syntax --- .../useCollectionCreate.ts | 12 +- .../useCollectionUpdate.tsx | 37 ++- .../useDashboardUpdateConfig.ts | 20 +- web/src/context/Users/useFavoriteProvider.tsx | 34 ++- web/src/hooks/useSocketQuery/mutationTypes.ts | 59 ++++ .../useSocketQuery/queryDataStrategies.ts | 48 +++ .../useSocketQuery/useSocketQueryMutation.tsx | 273 ++++-------------- 7 files changed, 223 insertions(+), 260 deletions(-) create mode 100644 web/src/hooks/useSocketQuery/mutationTypes.ts create mode 100644 web/src/hooks/useSocketQuery/queryDataStrategies.ts diff --git a/web/src/context/Collections/CollectionIndividualProvider/useCollectionCreate.ts b/web/src/context/Collections/CollectionIndividualProvider/useCollectionCreate.ts index 8282ba800..d2afd6219 100644 --- a/web/src/context/Collections/CollectionIndividualProvider/useCollectionCreate.ts +++ b/web/src/context/Collections/CollectionIndividualProvider/useCollectionCreate.ts @@ -31,12 +31,14 @@ export const useCollectionCreate = () => { { route: '/collections/delete' }, { route: '/collections/delete:deleteCollections' }, { - preSetQueryDataFunction: { - responseRoute: '/collections/list:listCollections', - callback: (data, variables) => { - return data?.filter((collection) => !variables.ids.includes(collection.id)) || []; + preSetQueryData: [ + { + responseRoute: '/collections/list:listCollections', + callback: (data, variables) => { + return data?.filter((collection) => !variables.ids.includes(collection.id)) || []; + } } - } + ] } ); diff --git a/web/src/context/Collections/CollectionIndividualProvider/useCollectionUpdate.tsx b/web/src/context/Collections/CollectionIndividualProvider/useCollectionUpdate.tsx index 66e087208..ef687ec64 100644 --- a/web/src/context/Collections/CollectionIndividualProvider/useCollectionUpdate.tsx +++ b/web/src/context/Collections/CollectionIndividualProvider/useCollectionUpdate.tsx @@ -8,23 +8,28 @@ export const useCollectionUpdate = () => { { route: '/collections/update' }, { route: '/collections/update:collectionState' }, { - preSetQueryData(data, _variables) { - const variables = _variables as Partial; - const newObject: BusterCollection = { ...data!, ...variables }; - return newObject; - }, - preSetQueryDataFunction: { - responseRoute: '/collections/list:listCollections', - callback: (data, _variables) => { - const existingData = data || []; - const variables = _variables as Partial; - return existingData.map((collection) => - collection.id === variables.id - ? { ...collection, name: variables.name || collection.name } - : collection - ); + preSetQueryData: [ + { + responseRoute: '/collections/get:collectionState', + callback: (data, _variables) => { + const variables = _variables as Partial; + const newObject: BusterCollection = { ...data!, ...variables }; + return newObject; + } + }, + { + responseRoute: '/collections/list:listCollections', + callback: (data, _variables) => { + const existingData = data || []; + const variables = _variables as Partial; + return existingData.map((collection) => + collection.id === variables.id + ? { ...collection, name: variables.name || collection.name } + : collection + ); + } } - } + ] } ); diff --git a/web/src/context/Dashboards/DashboardIndividualProvider/useDashboardUpdateConfig.ts b/web/src/context/Dashboards/DashboardIndividualProvider/useDashboardUpdateConfig.ts index 377fd15b1..57e3f2397 100644 --- a/web/src/context/Dashboards/DashboardIndividualProvider/useDashboardUpdateConfig.ts +++ b/web/src/context/Dashboards/DashboardIndividualProvider/useDashboardUpdateConfig.ts @@ -20,17 +20,19 @@ export const useDashboardUpdateConfig = ({ { route: '/dashboards/update' }, { route: '/dashboards/update:updateDashboard' }, { - preSetQueryDataFunction: { - responseRoute: '/dashboards/get:getDashboardState', - callback: (data, variables) => { - const newObject: BusterDashboardResponse = create(data!, (draft) => { - Object.assign(draft.dashboard, variables, { - config: { ...draft.dashboard.config, ...variables.config } + preSetQueryData: [ + { + responseRoute: '/dashboards/get:getDashboardState', + callback: (data, variables) => { + const newObject: BusterDashboardResponse = create(data!, (draft) => { + Object.assign(draft.dashboard, variables, { + config: { ...draft.dashboard.config, ...variables.config } + }); }); - }); - return newObject; + return newObject; + } } - } + ] } ); diff --git a/web/src/context/Users/useFavoriteProvider.tsx b/web/src/context/Users/useFavoriteProvider.tsx index f102cb85e..e98e9effd 100644 --- a/web/src/context/Users/useFavoriteProvider.tsx +++ b/web/src/context/Users/useFavoriteProvider.tsx @@ -10,15 +10,26 @@ export const useFavoriteProvider = () => { const { mutate: addItemToFavorite } = useSocketQueryMutation( { route: '/users/favorites/post' }, { route: '/users/favorites/post:createFavorite' }, - { preSetQueryData: (prev, mutationParams) => [mutationParams, ...(prev || [])] } + { + preSetQueryData: [ + { + responseRoute: '/users/favorites/list:listFavorites', + callback: (prev, mutationParams) => [mutationParams, ...(prev || [])] + } + ] + } ); const { mutate: removeItemFromFavorite } = useSocketQueryMutation( { route: '/users/favorites/delete' }, { route: '/users/favorites/post:createFavorite' }, { - preSetQueryData: (prev, mutationParams) => - prev?.filter((f) => f.id !== mutationParams.id) || [] + preSetQueryData: [ + { + responseRoute: '/users/favorites/list:listFavorites', + callback: (prev, mutationParams) => prev?.filter((f) => f.id !== mutationParams.id) || [] + } + ] } ); @@ -27,12 +38,17 @@ export const useFavoriteProvider = () => { { route: '/users/favorites/update:updateFavorite' }, { awaitPrefetchQueryData: true, - preSetQueryData: (prev, mutationParams) => { - return mutationParams.favorites.map((id, index) => { - let favorite = (prev || []).find((f) => f.id === id || f.collection_id === id)!; - return { ...favorite, index }; - }); - } + preSetQueryData: [ + { + responseRoute: '/users/favorites/list:listFavorites', + callback: (prev, mutationParams) => { + return mutationParams.favorites.map((id, index) => { + let favorite = (prev || []).find((f) => f.id === id || f.collection_id === id)!; + return { ...favorite, index }; + }); + } + } + ] } ); diff --git a/web/src/hooks/useSocketQuery/mutationTypes.ts b/web/src/hooks/useSocketQuery/mutationTypes.ts new file mode 100644 index 000000000..34b0cd336 --- /dev/null +++ b/web/src/hooks/useSocketQuery/mutationTypes.ts @@ -0,0 +1,59 @@ +import { UseMutationOptions } from '@tanstack/react-query'; +import { BusterSocketResponseRoute } from '@/api/buster_socket'; +import type { InferBusterSocketResponseData, BusterSocketRequestRoute } from './types'; + +export type QueryDataStrategy = 'replace' | 'append' | 'prepend' | 'merge' | 'ignore'; + +export type PreSetQueryDataItem = { + [Route in BusterSocketResponseRoute]: { + responseRoute: Route; + requestRoute?: BusterSocketRequestRoute; + callback: ( + data: InferBusterSocketResponseData, + variables: TVariables + ) => InferBusterSocketResponseData; + }; +}[BusterSocketResponseRoute]; + +export type SinglePreSetQueryDataItem = { + requestRoute?: BusterSocketRequestRoute; + callback: ( + data: InferBusterSocketResponseData, + variables: TVariables + ) => InferBusterSocketResponseData; +}; + +export type SocketQueryMutationOptions< + TRoute extends BusterSocketResponseRoute, + TError, + TVariables +> = Omit< + UseMutationOptions, TError, TVariables>, + 'mutationFn' +> & { + /** + * Configuration for optimistically updating query data before the mutation completes. + * Can be either a single item or an array of items. + */ + preSetQueryData?: + | Array> + | SinglePreSetQueryDataItem; + + /** + * When true, adds a small delay before applying preSetQueryData to ensure React Query's cache + * is properly initialized. + * @default false + */ + awaitPrefetchQueryData?: boolean; + + /** + * Strategy for integrating mutation response data into existing query data. + * @property 'replace' - Replace existing data + * @property 'append' - Add to end of array + * @property 'prepend' - Add to start of array + * @property 'merge' - Merge objects (requires ID field) + * @property 'ignore' - No automatic update + * @default 'ignore' + */ + queryDataStrategy?: QueryDataStrategy; +}; diff --git a/web/src/hooks/useSocketQuery/queryDataStrategies.ts b/web/src/hooks/useSocketQuery/queryDataStrategies.ts new file mode 100644 index 000000000..732a75de5 --- /dev/null +++ b/web/src/hooks/useSocketQuery/queryDataStrategies.ts @@ -0,0 +1,48 @@ +import { QueryClient } from '@tanstack/react-query'; +import { QueryDataStrategy } from './mutationTypes'; +import { BusterSocketResponseRoute } from '@/api/buster_socket'; +import { InferBusterSocketResponseData } from './types'; + +export const executeQueryDataStrategy = async ( + queryClient: QueryClient, + queryKey: unknown[], + data: InferBusterSocketResponseData, + strategy: QueryDataStrategy +) => { + if (strategy === 'ignore') return; + + const strategies: Record, () => Promise> = { + replace: async () => { + await queryClient.setQueryData(queryKey, data); + }, + append: async () => { + await queryClient.setQueryData[]>(queryKey, (prev) => [ + ...(Array.isArray(prev) ? prev : []), + data + ]); + }, + prepend: async () => { + await queryClient.setQueryData[]>(queryKey, (prev) => [ + data, + ...(Array.isArray(prev) ? prev : []) + ]); + }, + merge: async () => { + if (typeof data === 'object' && data !== null && 'id' in data) { + await queryClient.setQueryData>>( + queryKey, + (prev) => ({ + ...(prev || {}), + [(data as { id: string }).id]: data + }) + ); + } + } + }; + + const updateStrategy = strategies[strategy as Exclude]; + + if (updateStrategy) { + await updateStrategy(); + } +}; diff --git a/web/src/hooks/useSocketQuery/useSocketQueryMutation.tsx b/web/src/hooks/useSocketQuery/useSocketQueryMutation.tsx index c799b897d..5bbe86ffe 100644 --- a/web/src/hooks/useSocketQuery/useSocketQueryMutation.tsx +++ b/web/src/hooks/useSocketQuery/useSocketQueryMutation.tsx @@ -3,7 +3,7 @@ import { BusterSocketResponse, BusterSocketResponseRoute } from '@/api/buster_socket'; -import { QueryKey, useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useBusterWebSocket } from '@/context/BusterWebSocket'; import { useMemoizedFn } from 'ahooks'; import { @@ -13,105 +13,39 @@ import { BusterSocketRequestConfig, BusterSocketRequestRoute } from './types'; -import { ShareAssetType, BusterUserFavorite } from '@/api/asset_interfaces'; +import { SocketQueryMutationOptions } from './mutationTypes'; +import { executeQueryDataStrategy } from './queryDataStrategies'; import { createQueryKey } from './helpers'; -type QueryDataStrategy = 'replace' | 'append' | 'prepend' | 'merge' | 'ignore'; - -// For array items, each item's callback data type is inferred from its responseRoute -type PreSetQueryDataItem = { - [Route in BusterSocketResponseRoute]: { - responseRoute: Route; - requestRoute?: BusterSocketRequestRoute; - callback: ( - data: InferBusterSocketResponseData, - variables: TVariables - ) => InferBusterSocketResponseData; - }; -}[BusterSocketResponseRoute]; - -// For single items, callback data type is either from provided responseRoute or main route -type SinglePreSetQueryDataItem = { - requestRoute?: BusterSocketRequestRoute; - callback: ( - data: InferBusterSocketResponseData, - variables: TVariables - ) => InferBusterSocketResponseData; -}; - -type SocketQueryMutationOptions< - TRoute extends BusterSocketResponseRoute, - TError, - TVariables -> = Omit< - UseMutationOptions, TError, TVariables>, - 'mutationFn' -> & { - /** - * Configuration for optimistically updating query data before the mutation completes. - * Can be either a single item or an array of items. - * - * For array items: callback data type is inferred from each item's responseRoute - * For single item: - * - If responseRoute provided: callback data type is inferred from that route - * - If no responseRoute: callback data type is inferred from main response route - * - * @example - * // Single item without responseRoute (uses main response route's type) - * useSocketQueryMutation( - * { route: '/users/favorites/post' }, - * { route: '/users/favorites/post:createFavorite' }, - * { - * preSetQueryData: { - * callback: (existingFavorites, variables) => [...(existingFavorites || []), variables] - * } - * } - * ) - */ - preSetQueryData?: - | Array> - | SinglePreSetQueryDataItem; - - /** - * When true, adds a small delay before applying preSetQueryData to ensure React Query's cache - * is properly initialized. This is useful when the mutation is called immediately after - * component mount and you need to ensure the cache exists before updating it. - * - * @default false - */ - awaitPrefetchQueryData?: boolean; - - /** - * Determines how the mutation response data should be integrated into existing query data. - * - * @example - * // Example: Append new item to a list - * queryDataStrategy: 'append' - * - * @property 'replace' - Completely replace existing data with new data - * @property 'append' - Add new data to the end of an existing array - * @property 'prepend' - Add new data to the beginning of an existing array - * @property 'merge' - Merge new data with existing data (useful for objects with IDs) - * @property 'ignore' - Don't automatically update query data - * - * @default 'ignore' - */ - queryDataStrategy?: QueryDataStrategy; -}; - /** * A hook that creates a mutation for emitting socket requests and handling responses. + * Supports optimistic updates and various strategies for updating the query cache. * - * @template TRoute - The type of socket response route - * @template TData - The type of data returned by the socket response - * @template TVariables - The type of variables passed to the mutation function - * @template TError - The type of error that can occur + * @template TRequestRoute - The socket request route type + * @template TRoute - The socket response route type + * @template TError - The error type that can occur + * @template TVariables - The variables type passed to the mutation function * - * @param socketRequest - The base socket request to emit (variables will be merged with this) - * @param socketResponse - Configuration for the socket response including route and error handler - * @param options - Additional options for the React Query mutation hook + * @param socketRequest - The base socket request configuration + * @param socketResponse - The socket response configuration with optional error handler + * @param options - Additional options for configuring the mutation behavior * - * @returns A React Query mutation result for handling socket requests + * @example + * ```tsx + * const { mutate } = useSocketQueryMutation( + * { route: '/users/favorites/post' }, + * { route: '/users/favorites/post:createFavorite' }, + * { + * preSetQueryData: [ + * { + * responseRoute: '/users/favorites/list:listFavorites', + * callback: (data, variables) => [...(data || []), variables] + * } + * ], + * queryDataStrategy: 'append' + * } + * ); + * ``` */ export const useSocketQueryMutation = < TRequestRoute extends BusterSocketRequestRoute, @@ -129,45 +63,26 @@ export const useSocketQueryMutation = < const queryClient = useQueryClient(); const { preSetQueryData, queryDataStrategy = 'ignore', ...mutationOptions } = options || {}; - const updateQueryData = useMemoizedFn( - async (queryKey: QueryKey, data: InferBusterSocketResponseData) => { - if (queryDataStrategy === 'ignore') return; + const handlePreSetQueryData = useMemoizedFn(async (variables: TVariables) => { + if (!preSetQueryData) return; - const strategies: Record, () => Promise> = { - replace: async () => { - await queryClient.setQueryData(queryKey, data); - }, - append: async () => { - await queryClient.setQueryData[]>( - queryKey, - (prev) => [...(Array.isArray(prev) ? prev : []), data] - ); - }, - prepend: async () => { - await queryClient.setQueryData[]>( - queryKey, - (prev) => [data, ...(Array.isArray(prev) ? prev : [])] - ); - }, - merge: async () => { - if (typeof data === 'object' && data !== null && 'id' in data) { - await queryClient.setQueryData>>( - queryKey, - (prev) => ({ - ...(prev || {}), - [(data as { id: string }).id]: data - }) - ); - } - } - }; - - const updateStrategy = strategies[queryDataStrategy as Exclude]; - if (updateStrategy) { - await updateStrategy(); - } + if (options?.awaitPrefetchQueryData) { + await new Promise((resolve) => requestAnimationFrame(resolve)); } - ); + + const arrayOfPreSetQueryData = Array.isArray(preSetQueryData) + ? preSetQueryData + : [{ ...preSetQueryData, responseRoute: socketResponse.route }]; + + for (const item of arrayOfPreSetQueryData) { + const { responseRoute, requestRoute, callback } = item!; + const requestPayload: undefined | BusterSocketRequest = requestRoute + ? ({ route: requestRoute, payload: variables } as BusterSocketRequest) + : undefined; + const presetQueryKey = createQueryKey({ route: responseRoute! }, requestPayload); + await queryClient.setQueryData(presetQueryKey, (prev: any) => callback(prev, variables)); + } + }); const mutationFn = useMemoizedFn(async (variables: TVariables) => { const request = { @@ -176,33 +91,7 @@ export const useSocketQueryMutation = < } as BusterSocketRequest; const queryKey = createQueryKey(socketResponse, request); - const arrayOfPreSetQueryData = Array.isArray(preSetQueryData) - ? preSetQueryData - : preSetQueryData - ? [ - { - ...preSetQueryData, - responseRoute: socketResponse.route - } - ] - : []; - - if (preSetQueryData && arrayOfPreSetQueryData.filter(Boolean).length > 0) { - if (options?.awaitPrefetchQueryData) { - await new Promise((resolve) => requestAnimationFrame(resolve)); - } - - for (const item of arrayOfPreSetQueryData) { - const { responseRoute, requestRoute, callback } = item!; - const requestPayload: undefined | BusterSocketRequest = requestRoute - ? ({ route: requestRoute, payload: request.payload } as BusterSocketRequest) - : undefined; - const presetQueryKey = createQueryKey({ route: responseRoute! }, requestPayload); - await queryClient.setQueryData(presetQueryKey, (prev: any) => { - return callback(prev, variables); - }); - } - } + await handlePreSetQueryData(variables); const response = await busterSocket.emitAndOnce({ emitEvent: request, @@ -216,7 +105,12 @@ export const useSocketQueryMutation = < }); if (response !== undefined) { - await updateQueryData(queryKey, response as InferBusterSocketResponseData); + await executeQueryDataStrategy( + queryClient, + queryKey as unknown[], + response as InferBusterSocketResponseData, + queryDataStrategy + ); } return response as InferBusterSocketResponseData; @@ -227,66 +121,3 @@ export const useSocketQueryMutation = < mutationFn }); }; - -const Example = () => { - // Example: Favorites mutation with multiple preSetQueryData updates - const { mutate } = useSocketQueryMutation( - { route: '/users/favorites/post' }, - { route: '/users/favorites/post:createFavorite' }, - { - preSetQueryData: [ - { - responseRoute: '/users/favorites/list:listFavorites', - callback: (data, variables) => { - const favorites = Array.isArray(data) ? data : []; - return [variables, ...favorites]; - } - }, - { - responseRoute: '/users/favorites/post:createFavorite', - callback: (_: unknown, variables: BusterUserFavorite) => { - return [variables]; - } - } - ] - } - ); - - mutate({ - id: 'some-asset-id', - asset_type: ShareAssetType.DASHBOARD, - name: 'some-title' - }); - - const { mutate: mutate2 } = useSocketQueryMutation( - { route: '/dashboards/delete' }, - { route: '/dashboards/delete:deleteDashboard' }, - { - preSetQueryData: [ - { - responseRoute: '/chats/list:getChatsList', - callback: (data, variables) => { - data[0].id; - return [...(data || [])]; - } - } - ] - } - ); - - const { mutate: mutate3 } = useSocketQueryMutation( - { route: '/dashboards/delete' }, - { route: '/chats/get:getChat' }, - { - preSetQueryData: { - // responseRoute: '/chats/list:getChatsList', - callback: (data, variables) => { - data.id; - return { ...data }; - } - } - } - ); - - return null; -};