You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Beautiful DnD问题:后端获取文件夹后无法拖拽歌曲进入

问题描述

开发React应用时,使用react-beautiful-dnd实现歌曲与文件夹的拖拽功能,本地创建的文件夹可正常接收拖拽歌曲,但从后端获取的文件夹无法接收拖拽,也无悬停高亮效果。

异常现象

  • 后端获取的文件夹无法被识别为可放置区域,拖拽歌曲时无悬停高亮,无法完成拖拽操作
  • 本地创建的文件夹功能完全正常

简化代码结构

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

已尝试操作

  • 确认每个文件夹的droppableId设置正确
  • 验证DragDropContext与Droppable组件实现无误
  • 检查文件夹数据的获取与标准化处理正确

原因分析与解决方案

1. 文件夹ID类型不匹配

后端返回的文件夹ID可能是字符串类型,但代码中通过parseInt将droppableId拆分后得到数字类型,导致folder.id === folderId比较失败,同时react-beautiful-dnd内部依赖droppableId的类型一致性来识别可放置区域。

解决方法:

  • 统一ID类型:若后端返回字符串ID,修改handleDragEnd中的类型转换逻辑,保持与folder.id类型一致:
    // 假设后端返回的folder.id是字符串
    const folderId = destination.droppableId.split('-')[1] // 移除parseInt转换
    
  • 或在createStandardFolder工具函数中,将后端返回的ID转换为数字类型,确保与前端本地文件夹ID类型统一。

2. handleDragEnd依赖缺失

handleDragEnd中实际依赖localFolders完成更新,但useCallback的依赖数组仅包含localCatalog,导致组件更新后handleDragEnd仍引用旧的localFolders闭包,无法正确处理后端文件夹的更新。

解决方法:
将localFolders添加到useCallback的依赖数组中:

const handleDragEnd = useCallback((result: DropResult) => {
  // 原有逻辑保持不变
}, [localCatalog, localFolders])

3. Droppable标识不稳定

若后端返回的文件夹ID存在重复,或createStandardFolder处理后生成的ID不一致,会导致Droppable组件的key或droppableId混乱,react-beautiful-dnd无法正确跟踪可放置区域。

解决方法:

  • 确保后端返回的每个文件夹ID唯一,且createStandardFolder不会修改ID的唯一性
  • 检查droppableId生成逻辑,确保每个文件夹对应的droppableId唯一且与文件夹ID严格对应

4. 异步加载后上下文未重新注册

后端数据加载完成后,DragDropContext可能未正确注册新生成的Droppable组件,导致无法识别新的可放置区域。

解决方法:
给DragDropContext添加动态key,当localFolders更新时强制重新创建上下文:

<DragDropContext key={localFolders.length} onDragEnd={handleDragEnd}>
  <CatalogFolder folders={localFolders} />
  <SongList songs={localCatalog?.songs || []} />
</DragDropContext>

内容的提问来源于stack exchange,提问作者Sergey M.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 15:47:33