fix(tables): eliminate checkbox flicker on rapid cell toggle (#4592)

* fix(tables): eliminate checkbox flicker on rapid cell toggle

* fix(tables): symmetric guarded onSettled across row write mutations

* fix(tables): merge only mutated keys in onSuccess to preserve concurrent optimistic patches
This commit is contained in:
Waleed
2026-05-13 19:52:43 -07:00
committed by GitHub
parent 568a552d67
commit 104949bdc2
+31 -6
View File
@@ -85,6 +85,7 @@ export const tableKeys = {
rowsRoot: (tableId: string) => [...tableKeys.detail(tableId), 'rows'] as const,
infiniteRows: (tableId: string, paramsKey: string) =>
[...tableKeys.rowsRoot(tableId), 'infinite', paramsKey] as const,
rowWrites: (tableId: string) => [...tableKeys.rowsRoot(tableId), 'write'] as const,
}
type TableRowsParams = Omit<TableRowsQueryInput, 'filter' | 'sort'> &
@@ -543,14 +544,15 @@ export function useUpdateTableRow({ workspaceId, tableId }: RowMutationContext)
const queryClient = useQueryClient()
return useMutation({
mutationKey: tableKeys.rowWrites(tableId),
mutationFn: async ({ rowId, data }: UpdateTableRowParams) => {
return requestJson(updateTableRowContract, {
params: { tableId, rowId },
body: { workspaceId, data: data as RowData },
})
},
onMutate: ({ rowId, data }) => {
void queryClient.cancelQueries({ queryKey: tableKeys.rowsRoot(tableId) })
onMutate: async ({ rowId, data }) => {
await queryClient.cancelQueries({ queryKey: tableKeys.rowsRoot(tableId) })
const previousQueries = queryClient.getQueriesData<InfiniteData<TableRowsResponse, number>>({
queryKey: tableKeys.rowsRoot(tableId),
@@ -573,6 +575,24 @@ export function useUpdateTableRow({ workspaceId, tableId }: RowMutationContext)
return { previousQueries }
},
onSuccess: (response, { rowId, data: mutatedData }) => {
const serverRow = response.data.row
const mutatedKeys = Object.keys(mutatedData)
patchCachedRows(queryClient, tableId, (row) => {
if (row.id !== rowId) return row
const merged: RowData = { ...row.data }
for (const key of mutatedKeys) {
merged[key] = (serverRow.data as RowData)[key]
}
return {
...row,
data: merged,
position: serverRow.position,
createdAt: serverRow.createdAt,
updatedAt: serverRow.updatedAt,
}
})
},
onError: (error, _vars, context) => {
if (context?.previousQueries) {
for (const [queryKey, data] of context.previousQueries) {
@@ -583,7 +603,9 @@ export function useUpdateTableRow({ workspaceId, tableId }: RowMutationContext)
toast.error(error.message, { duration: 5000 })
},
onSettled: () => {
invalidateRowData(queryClient, tableId)
if (queryClient.isMutating({ mutationKey: tableKeys.rowWrites(tableId) }) === 1) {
invalidateRowData(queryClient, tableId)
}
},
})
}
@@ -599,6 +621,7 @@ export function useBatchUpdateTableRows({ workspaceId, tableId }: RowMutationCon
const queryClient = useQueryClient()
return useMutation({
mutationKey: tableKeys.rowWrites(tableId),
mutationFn: async ({ updates }: BatchUpdateTableRowsParams) => {
return requestJson(batchUpdateTableRowsContract, {
params: { tableId },
@@ -608,8 +631,8 @@ export function useBatchUpdateTableRows({ workspaceId, tableId }: RowMutationCon
},
})
},
onMutate: ({ updates }) => {
void queryClient.cancelQueries({ queryKey: tableKeys.rowsRoot(tableId) })
onMutate: async ({ updates }) => {
await queryClient.cancelQueries({ queryKey: tableKeys.rowsRoot(tableId) })
const previousQueries = queryClient.getQueriesData<InfiniteData<TableRowsResponse, number>>({
queryKey: tableKeys.rowsRoot(tableId),
@@ -644,7 +667,9 @@ export function useBatchUpdateTableRows({ workspaceId, tableId }: RowMutationCon
toast.error(error.message, { duration: 5000 })
},
onSettled: () => {
invalidateRowData(queryClient, tableId)
if (queryClient.isMutating({ mutationKey: tableKeys.rowWrites(tableId) }) === 1) {
invalidateRowData(queryClient, tableId)
}
},
})
}