update to use new syntax

This commit is contained in:
Nate Kelley 2025-02-12 23:53:25 -07:00
parent bdd1cbd64c
commit f32bbf0a75
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
7 changed files with 223 additions and 260 deletions

View File

@ -31,12 +31,14 @@ export const useCollectionCreate = () => {
{ route: '/collections/delete' }, { route: '/collections/delete' },
{ route: '/collections/delete:deleteCollections' }, { route: '/collections/delete:deleteCollections' },
{ {
preSetQueryDataFunction: { preSetQueryData: [
responseRoute: '/collections/list:listCollections', {
callback: (data, variables) => { responseRoute: '/collections/list:listCollections',
return data?.filter((collection) => !variables.ids.includes(collection.id)) || []; callback: (data, variables) => {
return data?.filter((collection) => !variables.ids.includes(collection.id)) || [];
}
} }
} ]
} }
); );

View File

@ -8,23 +8,28 @@ export const useCollectionUpdate = () => {
{ route: '/collections/update' }, { route: '/collections/update' },
{ route: '/collections/update:collectionState' }, { route: '/collections/update:collectionState' },
{ {
preSetQueryData(data, _variables) { preSetQueryData: [
const variables = _variables as Partial<BusterCollection>; {
const newObject: BusterCollection = { ...data!, ...variables }; responseRoute: '/collections/get:collectionState',
return newObject; callback: (data, _variables) => {
}, const variables = _variables as Partial<BusterCollection>;
preSetQueryDataFunction: { const newObject: BusterCollection = { ...data!, ...variables };
responseRoute: '/collections/list:listCollections', return newObject;
callback: (data, _variables) => { }
const existingData = data || []; },
const variables = _variables as Partial<BusterCollection>; {
return existingData.map((collection) => responseRoute: '/collections/list:listCollections',
collection.id === variables.id callback: (data, _variables) => {
? { ...collection, name: variables.name || collection.name } const existingData = data || [];
: collection const variables = _variables as Partial<BusterCollection>;
); return existingData.map((collection) =>
collection.id === variables.id
? { ...collection, name: variables.name || collection.name }
: collection
);
}
} }
} ]
} }
); );

View File

@ -20,17 +20,19 @@ export const useDashboardUpdateConfig = ({
{ route: '/dashboards/update' }, { route: '/dashboards/update' },
{ route: '/dashboards/update:updateDashboard' }, { route: '/dashboards/update:updateDashboard' },
{ {
preSetQueryDataFunction: { preSetQueryData: [
responseRoute: '/dashboards/get:getDashboardState', {
callback: (data, variables) => { responseRoute: '/dashboards/get:getDashboardState',
const newObject: BusterDashboardResponse = create(data!, (draft) => { callback: (data, variables) => {
Object.assign(draft.dashboard, variables, { const newObject: BusterDashboardResponse = create(data!, (draft) => {
config: { ...draft.dashboard.config, ...variables.config } Object.assign(draft.dashboard, variables, {
config: { ...draft.dashboard.config, ...variables.config }
});
}); });
}); return newObject;
return newObject; }
} }
} ]
} }
); );

View File

@ -10,15 +10,26 @@ export const useFavoriteProvider = () => {
const { mutate: addItemToFavorite } = useSocketQueryMutation( const { mutate: addItemToFavorite } = useSocketQueryMutation(
{ route: '/users/favorites/post' }, { route: '/users/favorites/post' },
{ route: '/users/favorites/post:createFavorite' }, { 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( const { mutate: removeItemFromFavorite } = useSocketQueryMutation(
{ route: '/users/favorites/delete' }, { route: '/users/favorites/delete' },
{ route: '/users/favorites/post:createFavorite' }, { route: '/users/favorites/post:createFavorite' },
{ {
preSetQueryData: (prev, mutationParams) => preSetQueryData: [
prev?.filter((f) => f.id !== mutationParams.id) || [] {
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' }, { route: '/users/favorites/update:updateFavorite' },
{ {
awaitPrefetchQueryData: true, awaitPrefetchQueryData: true,
preSetQueryData: (prev, mutationParams) => { preSetQueryData: [
return mutationParams.favorites.map((id, index) => { {
let favorite = (prev || []).find((f) => f.id === id || f.collection_id === id)!; responseRoute: '/users/favorites/list:listFavorites',
return { ...favorite, index }; callback: (prev, mutationParams) => {
}); return mutationParams.favorites.map((id, index) => {
} let favorite = (prev || []).find((f) => f.id === id || f.collection_id === id)!;
return { ...favorite, index };
});
}
}
]
} }
); );

View File

@ -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<TVariables> = {
[Route in BusterSocketResponseRoute]: {
responseRoute: Route;
requestRoute?: BusterSocketRequestRoute;
callback: (
data: InferBusterSocketResponseData<Route>,
variables: TVariables
) => InferBusterSocketResponseData<Route>;
};
}[BusterSocketResponseRoute];
export type SinglePreSetQueryDataItem<TRoute extends BusterSocketResponseRoute, TVariables> = {
requestRoute?: BusterSocketRequestRoute;
callback: (
data: InferBusterSocketResponseData<TRoute>,
variables: TVariables
) => InferBusterSocketResponseData<TRoute>;
};
export type SocketQueryMutationOptions<
TRoute extends BusterSocketResponseRoute,
TError,
TVariables
> = Omit<
UseMutationOptions<InferBusterSocketResponseData<TRoute>, 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<PreSetQueryDataItem<TVariables>>
| SinglePreSetQueryDataItem<TRoute, TVariables>;
/**
* 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;
};

View File

@ -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 <TRoute extends BusterSocketResponseRoute>(
queryClient: QueryClient,
queryKey: unknown[],
data: InferBusterSocketResponseData<TRoute>,
strategy: QueryDataStrategy
) => {
if (strategy === 'ignore') return;
const strategies: Record<Exclude<QueryDataStrategy, 'ignore'>, () => Promise<void>> = {
replace: async () => {
await queryClient.setQueryData(queryKey, data);
},
append: async () => {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>[]>(queryKey, (prev) => [
...(Array.isArray(prev) ? prev : []),
data
]);
},
prepend: async () => {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>[]>(queryKey, (prev) => [
data,
...(Array.isArray(prev) ? prev : [])
]);
},
merge: async () => {
if (typeof data === 'object' && data !== null && 'id' in data) {
await queryClient.setQueryData<Record<string, InferBusterSocketResponseData<TRoute>>>(
queryKey,
(prev) => ({
...(prev || {}),
[(data as { id: string }).id]: data
})
);
}
}
};
const updateStrategy = strategies[strategy as Exclude<QueryDataStrategy, 'ignore'>];
if (updateStrategy) {
await updateStrategy();
}
};

View File

@ -3,7 +3,7 @@ import {
BusterSocketResponse, BusterSocketResponse,
BusterSocketResponseRoute BusterSocketResponseRoute
} from '@/api/buster_socket'; } 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 { useBusterWebSocket } from '@/context/BusterWebSocket';
import { useMemoizedFn } from 'ahooks'; import { useMemoizedFn } from 'ahooks';
import { import {
@ -13,105 +13,39 @@ import {
BusterSocketRequestConfig, BusterSocketRequestConfig,
BusterSocketRequestRoute BusterSocketRequestRoute
} from './types'; } from './types';
import { ShareAssetType, BusterUserFavorite } from '@/api/asset_interfaces'; import { SocketQueryMutationOptions } from './mutationTypes';
import { executeQueryDataStrategy } from './queryDataStrategies';
import { createQueryKey } from './helpers'; 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<TVariables> = {
[Route in BusterSocketResponseRoute]: {
responseRoute: Route;
requestRoute?: BusterSocketRequestRoute;
callback: (
data: InferBusterSocketResponseData<Route>,
variables: TVariables
) => InferBusterSocketResponseData<Route>;
};
}[BusterSocketResponseRoute];
// For single items, callback data type is either from provided responseRoute or main route
type SinglePreSetQueryDataItem<TRoute extends BusterSocketResponseRoute, TVariables> = {
requestRoute?: BusterSocketRequestRoute;
callback: (
data: InferBusterSocketResponseData<TRoute>,
variables: TVariables
) => InferBusterSocketResponseData<TRoute>;
};
type SocketQueryMutationOptions<
TRoute extends BusterSocketResponseRoute,
TError,
TVariables
> = Omit<
UseMutationOptions<InferBusterSocketResponseData<TRoute>, 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<PreSetQueryDataItem<TVariables>>
| SinglePreSetQueryDataItem<TRoute, TVariables>;
/**
* 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. * 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 TRequestRoute - The socket request route type
* @template TData - The type of data returned by the socket response * @template TRoute - The socket response route type
* @template TVariables - The type of variables passed to the mutation function * @template TError - The error type that can occur
* @template TError - The type of error 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 socketRequest - The base socket request configuration
* @param socketResponse - Configuration for the socket response including route and error handler * @param socketResponse - The socket response configuration with optional error handler
* @param options - Additional options for the React Query mutation hook * @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 = < export const useSocketQueryMutation = <
TRequestRoute extends BusterSocketRequestRoute, TRequestRoute extends BusterSocketRequestRoute,
@ -129,45 +63,26 @@ export const useSocketQueryMutation = <
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { preSetQueryData, queryDataStrategy = 'ignore', ...mutationOptions } = options || {}; const { preSetQueryData, queryDataStrategy = 'ignore', ...mutationOptions } = options || {};
const updateQueryData = useMemoizedFn( const handlePreSetQueryData = useMemoizedFn(async (variables: TVariables) => {
async (queryKey: QueryKey, data: InferBusterSocketResponseData<TRoute>) => { if (!preSetQueryData) return;
if (queryDataStrategy === 'ignore') return;
const strategies: Record<Exclude<QueryDataStrategy, 'ignore'>, () => Promise<void>> = { if (options?.awaitPrefetchQueryData) {
replace: async () => { await new Promise((resolve) => requestAnimationFrame(resolve));
await queryClient.setQueryData(queryKey, data);
},
append: async () => {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>[]>(
queryKey,
(prev) => [...(Array.isArray(prev) ? prev : []), data]
);
},
prepend: async () => {
await queryClient.setQueryData<InferBusterSocketResponseData<TRoute>[]>(
queryKey,
(prev) => [data, ...(Array.isArray(prev) ? prev : [])]
);
},
merge: async () => {
if (typeof data === 'object' && data !== null && 'id' in data) {
await queryClient.setQueryData<Record<string, InferBusterSocketResponseData<TRoute>>>(
queryKey,
(prev) => ({
...(prev || {}),
[(data as { id: string }).id]: data
})
);
}
}
};
const updateStrategy = strategies[queryDataStrategy as Exclude<QueryDataStrategy, 'ignore'>];
if (updateStrategy) {
await updateStrategy();
}
} }
);
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 mutationFn = useMemoizedFn(async (variables: TVariables) => {
const request = { const request = {
@ -176,33 +91,7 @@ export const useSocketQueryMutation = <
} as BusterSocketRequest; } as BusterSocketRequest;
const queryKey = createQueryKey(socketResponse, request); const queryKey = createQueryKey(socketResponse, request);
const arrayOfPreSetQueryData = Array.isArray(preSetQueryData) await handlePreSetQueryData(variables);
? 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);
});
}
}
const response = await busterSocket.emitAndOnce({ const response = await busterSocket.emitAndOnce({
emitEvent: request, emitEvent: request,
@ -216,7 +105,12 @@ export const useSocketQueryMutation = <
}); });
if (response !== undefined) { if (response !== undefined) {
await updateQueryData(queryKey, response as InferBusterSocketResponseData<TRoute>); await executeQueryDataStrategy(
queryClient,
queryKey as unknown[],
response as InferBusterSocketResponseData<TRoute>,
queryDataStrategy
);
} }
return response as InferBusterSocketResponseData<TRoute>; return response as InferBusterSocketResponseData<TRoute>;
@ -227,66 +121,3 @@ export const useSocketQueryMutation = <
mutationFn 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;
};