I'm working on a React application using react-beautiful-dnd to implement drag-and-drop functionality for songs and folders. The drag-and-drop works perfectly with locally created folders, but I'm encountering issues when trying to drop songs into folders that are fetched from the backend. After fetching, the folders become undroppable, and there's no hover effect.
Here's a simplified version of my setup:
CatalogComponent.tsx
import React, { useCallback, useEffect, useState } from 'react'
import { DragDropContext, DropResult } from 'react-beautiful-dnd'
import { useDispatch } from 'react-redux'
import { fetchCatalog, fetchFolders } from './catalogSlice'
import { unwrapResult } from '@reduxjs/toolkit'
import { toast } from 'react-toastify'
import CatalogFolder from './CatalogFolder'
import SongList from './SongList'
import { Folder, Song } from '../../../app/layout/models/catalog'
import { createStandardFolder, getFolderDroppableId } from '../../../app/layout/utils/folderUtils'
export default function CatalogComponent() {
const dispatch = useDispatch()
const [localCatalog, setLocalCatalog] = useState<{ songs: Song[] } | null>(null)
const [localFolders, setLocalFolders] = useState<Folder[]>([])
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
try {
const [catalogResult, foldersResult] = await Promise.all([
dispatch(fetchCatalog()),
dispatch(fetchFolders()),
])
const catalogData = unwrapResult(catalogResult)
const foldersData = unwrapResult(foldersResult)
const standardizedFolders = foldersData.map(createStandardFolder)
setLocalCatalog(catalogData)
setLocalFolders(standardizedFolders)
} catch (error) {
toast.error('Error fetching data')
} finally {
setIsLoading(false)
}
}
fetchData()
}, [dispatch])
const handleDragEnd = useCallback((result: DropResult) => {
if (!result.destination) return
const { source, destination, draggableId } = result
if (source.droppableId === 'song-list' && destination.droppableId.startsWith('folder-')) {
const folderId = parseInt(destination.droppableId.split('-')[1], 10)
const songId = parseInt(draggableId, 10)
const song = localCatalog?.songs.find(s => s.songId === songId)
if (!song) return
setLocalFolders(prevFolders => {
return prevFolders.map(folder => {
if (folder.id === folderId) {
return {
...folder,
nodes: [...folder.nodes, { id: Date.now(), songId: song.songId, name: song.name, nodes: [] }]
}
}
return folder
})
})
}
}, [localCatalog])
if (isLoading) return <div>Loading...</div>
return (
<DragDropContext onDragEnd={handleDragEnd}>
<CatalogFolder folders={localFolders} />
<SongList songs={localCatalog?.songs || []} />
</DragDropContext>
)
}
CatalogFolder.tsx
import React from 'react'
import { Droppable } from 'react-beautiful-dnd'
import { Folder } from '../../../app/layout/models/catalog'
interface Props {
folders: Folder[]
}
const CatalogFolder: React.FC<Props> = ({ folders }) => {
return (
<div>
{folders.map(folder => (
<Droppable key={folder.id} droppableId={`folder-${folder.id}`}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={{
background: snapshot.isDraggingOver ? 'lightblue' : 'lightgrey',
padding: 4,
width: 250,
minHeight: 500,
}}
>
<h4>{folder.name}</h4>
{provided.placeholder}
</div>
)}
</Droppable>
))}
</div>
)
}
export default CatalogFolder
Problem:
What I've Tried:
Question: What could be causing the fetched folders to become undroppable, and how can I resolve this issue?
I found the solution to my problem with the drag-and-drop functionality in React 18. The issue was related to the use of Droppable from react-beautiful-dnd. In React 18, it's recommended to use a custom StrictModeDroppable component to handle the strict mode rendering behavior.
Here's the custom StrictModeDroppable component I used:
import React, { useEffect, useState } from 'react'
import { Droppable, DroppableProps } from 'react-beautiful-dnd'
export const StrictModeDroppable: React.FC<DroppableProps> = ({
children,
...props
}) => {
const [enabled, setEnabled] = useState(false)
useEffect(() => {
const animation = requestAnimationFrame(() => setEnabled(true))
return () => {
cancelAnimationFrame(animation)
setEnabled(false)
}
}, [])
if (!enabled) {
return null
}
return <Droppable {...props}>{children}</Droppable>
}
How It Works:
Implementation: I've replace the standard Droppable with StrictModeDroppable in my component:
import { StrictModeDroppable } from './path/to/StrictModeDroppable'
// Usage in CatalogFolder component
<StrictModeDroppable droppableId={`folder-${folder.id}`}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
style={{
background: snapshot.isDraggingOver ? 'lightblue' : 'lightgrey',
padding: 4,
width: 250,
minHeight: 500,
}}
>
<h4>{folder.name}</h4>
{provided.placeholder}
</div>
)}
</StrictModeDroppable>
Conclusion: By using StrictModeDroppable, I was able to resolve the issue with dropping songs into folders after fetching them from the backend. This approach ensures compatibility with React 18's strict mode and maintains the expected drag-and-drop functionality.
I hope this helps others facing similar issues!