GB-
GB-•3mo ago

Dnd-kit and Mutations

Hi. I'm quite new to Convex and Dnd-kit. Hopefully someone can help me, I'm trying to create a UI where the user can rearrange a sortable list. The sortable list works great when just updating the UI, but when I add a mutation to the onDragEnd function, it seems like the Convex query is identifying that the data has changed and it refreshes each of the items in the list. I've tried using withOptimisticUpdate but that hasn't made any difference. Is this a common problem? Any pointers?
2 Replies
GB-
GB-OP•3mo ago
I asked AI to create a simplified example of my code to demonstrate what I'm trying to achieve.
'use client'

import { useQuery, useMutation } from "convex/react"
import { DragDropProvider } from '@dnd-kit/react'
import { useSortable } from '@dnd-kit/react/sortable'

// Simple item type
interface Item {
_id: string
title: string
color: string
}

// Simple draggable item component
function DraggableItem({ item, index }: { item: Item, index: number }) {
const sortable = useSortable({
id: item._id,
index,
data: { item }
})

return (
<div
ref={sortable.ref}
style={{
backgroundColor: item.color,
padding: '20px',
margin: '10px',
borderRadius: '8px',
opacity: sortable.isDragging ? 0.5 : 1
}}
>
<h3>{item.title}</h3>
</div>
)
}

// Main component
export default function DragExample() {
// Simulate Convex query
const items = useQuery('items:getAll') as Item[] || []

// Simulate Convex mutation with optimistic update
const moveItem = useMutation('items:move')
.withOptimisticUpdate((localStore, args) => {
const { itemId, sourceIndex, targetIndex } = args

// Get current items
const currentItems = localStore.getQuery('items:getAll')
if (!currentItems) return

// Create new array with reordered items
const newItems = [...currentItems]
const [movedItem] = newItems.splice(sourceIndex, 1)
newItems.splice(targetIndex, 0, movedItem)

// Update the query
localStore.setQuery('items:getAll', {}, newItems)
})
'use client'

import { useQuery, useMutation } from "convex/react"
import { DragDropProvider } from '@dnd-kit/react'
import { useSortable } from '@dnd-kit/react/sortable'

// Simple item type
interface Item {
_id: string
title: string
color: string
}

// Simple draggable item component
function DraggableItem({ item, index }: { item: Item, index: number }) {
const sortable = useSortable({
id: item._id,
index,
data: { item }
})

return (
<div
ref={sortable.ref}
style={{
backgroundColor: item.color,
padding: '20px',
margin: '10px',
borderRadius: '8px',
opacity: sortable.isDragging ? 0.5 : 1
}}
>
<h3>{item.title}</h3>
</div>
)
}

// Main component
export default function DragExample() {
// Simulate Convex query
const items = useQuery('items:getAll') as Item[] || []

// Simulate Convex mutation with optimistic update
const moveItem = useMutation('items:move')
.withOptimisticUpdate((localStore, args) => {
const { itemId, sourceIndex, targetIndex } = args

// Get current items
const currentItems = localStore.getQuery('items:getAll')
if (!currentItems) return

// Create new array with reordered items
const newItems = [...currentItems]
const [movedItem] = newItems.splice(sourceIndex, 1)
newItems.splice(targetIndex, 0, movedItem)

// Update the query
localStore.setQuery('items:getAll', {}, newItems)
})
const handleDragEnd = async (event: any) => {
const { operation, canceled } = event
if (canceled) return

if (operation.target) {
await moveItem({
itemId: operation.source.data.item._id,
sourceIndex: operation.source.index,
targetIndex: operation.target.sortable.index
})
}
}

return (
<DragDropProvider onDragEnd={handleDragEnd}>
<div style={{ padding: '20px' }}>
{items.map((item, index) => (
<DraggableItem key={item._id} item={item} index={index} />
))}
</div>
</DragDropProvider>
)
}
const handleDragEnd = async (event: any) => {
const { operation, canceled } = event
if (canceled) return

if (operation.target) {
await moveItem({
itemId: operation.source.data.item._id,
sourceIndex: operation.source.index,
targetIndex: operation.target.sortable.index
})
}
}

return (
<DragDropProvider onDragEnd={handleDragEnd}>
<div style={{ padding: '20px' }}>
{items.map((item, index) => (
<DraggableItem key={item._id} item={item} index={index} />
))}
</div>
</DragDropProvider>
)
}
Gemini Pro solved it for me. Optimistic updates were not the solution. The issue was that the items[] is an array of objects that include an itemId. There's then a query to get the item details for each idemId, which is an object, let's call it itemDetails{}. I'm then passing the items[] and itemDetails{} to render. When the order of items[] changes, items[] is briefly undefined and the itemDetails{} gets rebuilt as an empty object and that causes all the items in the list to go blank. The solution was to create a second array of items, sorted by itemID, call it sortedItemIds[]. That way, itemDetails{} will won't update because the order of items changes. I still need to improve it so that the existing items don't flash when a new item is added, but I think I can crack this one 😄
Nerdkidchiki
Nerdkidchiki•3mo ago
Nice one 🤌 I also am currently building a Kanban application using Convex , but i used HelloPangea/React for Drag and Drop Here is the Link to the Repo: https://github.com/Muibeabuchi/STRIDE
GitHub
GitHub - Muibeabuchi/STRIDE: A collaborative tool for managing proj...
A collaborative tool for managing projects simiilar to JIRA. Built using TANSTACKSTART(BETA) and CONVEX. - GitHub - Muibeabuchi/STRIDE: A collaborative tool for managing projects simiilar to JIRA....

Did you find this page helpful?