Using React Beautiful DnD Issues

Hello, I would like to implement Drag & Drop functionality into my app. I am using code I have already written from a regular React project where it works perfectly. All looks good except for console errors when I am dragging and dropping:

Thank you in advance for any help,

Lisa

Hi There,

It would be best if you could share your code with us, but if you can’t here is some code snippets from a project I have that uses Beautiful DND. I hope it help.

From a component that shows a list of videos that can be re-ordered. This is in a cell…



const UPDATE_PLAYLIST_ORDER = gql`
  mutation UpdatePlayListOrder($id: Int!, $input: UpdatePlaylistOrderInput!) {
    updatePlaylistOrder(id: $id, input: $input) {
      id
      title
      videos {
        id
        order
        video {
          title
          createdAt
          playbackId
        }
      }
    }
  }
`

 const [updatePlaylistOrder] = useMutation(UPDATE_PLAYLIST_ORDER, {
    refetchQueries: ['FindPlaylistQuery'],
    onCompleted: () => {
      toast.success('Playlist order updated')
    },
    onError: (error) => {
      toast.error(error.message)
    },
  })

  const ondragEnd = (result) => {
    const { destination, source } = result

    if (!destination) {
      return
    }

    if (
      destination.droppableId === source.droppableId &&
      destination.index === source.index
    ) {
      return
    }

    const newVideos = Array.from(playlist.videos)
    newVideos.splice(source.index, 1)
    newVideos.splice(destination.index, 0, playlist.videos[source.index])
    const videosForMutation = newVideos.map((video, index) => {
      return {
        id: video.id,
        order: index,
      }
    })

    updatePlaylistOrder({
      variables: { id: playlist.id, input: { videos: videosForMutation } },
      optimisticResponse: {
        updatePlaylistOrder: {
          id: playlist.id,
          __typename: 'Playlist',
          title: playlist.title,
          videos: newVideos.map((video) => {
            return {
              id: video.id,
              order: video.order,
              video: {
                title: video.video.title,
                createdAt: video.video.createdAt,
                playbackId: video.video.playbackId,
              },
            }
          }),
        },
      },
    })
  }

<DragDropContext onDragEnd={ondragEnd}>
        <Droppable droppableId={'1'}>
          {(provided) => (
            <VStack
              divider={<Divider borderColor="#9A9A9A" my={3} />}
              align="stretch"
              width="100%"
              {...provided.droppableProps}
              ref={provided.innerRef}
            >
              {playlist.videos.map((video, index) => {
                return (
                  <PlaylistVideo
                    key={video.id}
                    id={video.id}
                    playbackId={video.video.playbackId}
                    title={video.video.title}
                    createdAt={video.video.createdAt}
                    index={index}
                    playlistId={playlist.id}
                  />
                )
              })}
              {provided.placeholder}
            </VStack>
          )}
        </Droppable>
      </DragDropContext>

And then the individual component

const PlaylistVideo: React.FC<Props> = ({
  playbackId,
  title,
  createdAt,
  index,
  id,
  playlistId,
}) => {
  return (
    <Draggable draggableId={id.toString()} index={index}>
      {(provided, snapshot) => {
        return (
          <Flex
            {...provided.draggableProps}
            {...provided.dragHandleProps}
            ref={provided.innerRef}
            bg={
              snapshot.isDragging
                ? 'secondary.grey.main'
                : 'secondary.grey.light'
            }
            px={4}
            py={3}
          >
            <Box
              mr={4}
              flex={{ base: '0 0 200px', xl: '0 0 311px' }}
              borderRadius={8}
              overflow="hidden"
              sx={{ aspectRatio: '16/9' }}
            >
              <MuxPlayer
                style={{
                  maxWidth: '311px',
                  aspectRatio: '16/9',
                  width: '100%',
                }}
                streamType="on-demand"
                playbackId={playbackId}
                preferMse
                metadata={{
                  video_id: playbackId,
                  video_title: `${title}`,
                }}
                playsInline
              />
            </Box>
            <Box flex="1 0 300px">
              <Heading
                as="h2"
                fontWeight="semibold"
                fontSize="lg"
                color="primary.darkBlue.main"
              >
                {title}
              </Heading>
              <Text fontSize="md" color="#6C6B6B" mt={1}>
                {new Intl.DateTimeFormat('en-GB', {
                  year: 'numeric',
                  month: 'long',
                  day: 'numeric',
                }).format(new Date(createdAt))}
              </Text>
            </Box>
            <Flex
              direction="column"
              justifyContent="space-around"
              alignItems="center"
            >
              <PlaylistDragIcon />
              <DeletePlaylistVideo playlistId={playlistId} videoId={id} />
            </Flex>
          </Flex>
        )
      }}
    </Draggable>
  )
}

Thank you for this code! Mine is similar, but I have not added the useMutation query to persist the change in order yet. The code is erroring in the onDragEnd function where the newItems array gets spliced.

This code runs perfect outside of Redwood in a plain React application. I am not persisting the data set there either.

The code is for moving rows and items within each row.

export const Success = ({ planRows }) => {
  let planData = { plans: [] }
  const [items, setItems] = useState({ ...planData, plans: planRows })

  function onDragEnd(props) {
    const { source, destination, type } = props
    if (!destination) {
      return
    }

    const sourceIndex = source.index
    const destIndex = destination.index
    const sourceParentId = parseInt(source.droppableId, 0)
    const destParentId = parseInt(destination.droppableId, 0)

    if (type === 'planItem') {
      const newItems = {
        ...items,
      }

      const item = newItems[source.droppableId][sourceIndex]

      // The two lines below cause the console errors posted in the screen shot
      newItems[source.droppableId].splice(sourceIndex, 1)
      newItems[destination.droppableId].splice(destIndex, 0, item)

      return setItems(newItems)
    }

    if (sourceParentId === destParentId) {
      const newItems = {
        ...items,
      }
      const plan = type.split('-')[0]

      const planItem = newItems[plan]
        .map((item) => item.id)
        .indexOf(sourceParentId)
      const item = newItems[plan][planItem].planItems[sourceIndex]

       // These two lines below cause the same console.errors
      newItems[plan][planItem].planItems.splice(sourceIndex, 1)
      newItems[plan][planItem].planItems.splice(destIndex, 0, item)

      return setItems(newItems)
    }
  }
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div>
        {Object.keys(items).map((plan) => (
          <div key={plan.id}>
            <PlanRow data={items[plan]} type={plan} />
          </div>
        ))}
      </div>
    </DragDropContext>
  )
}

When I see Cannot delete property '2' of Array it looks like the array index is a string and not a number, is that possible? It seems like sourceIndex needs to get parseInt()ified maybe?

Not sure why props.source given to onDragEnd() would have a different format in Redwood versus any other app though…

That is a great question! The sourceIndex has a type of number

Can you say more about your comment/question on props.source being give to onDragEnd ?

Are there maybe not 3 elements in that array, so trying to delete the third one is failing? It does say “Cannot delete” and if you call splice() with only two arguments, it just deletes the element in that index position (as opposed to inserting a new value when you call it with three arguments).

What if you console.log(newItems[source.droppableId]) and console.log(sourceIndex) right before that line that fails?

Can you say more about your comment/question on props.source being give to onDragEnd ?

Well, that was based on my bad assumption: I assumed that source.index was a string, but if it worked in your other app then it must have been a number there, so I couldn’t figure out why those props would have different types in one app versus another. But if it’s already a number then it destroys my theory! :slight_smile:

Correct. I have been reading up on .splice() but it works exactly as I understand it.

I am dumb: this block has the wrong type on l84.

HMMM so newItems[source.droppableId] is from GraphQL? In Redwood that means it comes from the Apollo cache, and those values are read-only! TypeError: Cannot assign to read only property · Issue #5903 · apollographql/apollo-client · GitHub

Can you make a copy of newItems[source.droppableId] before splicing it? Doing a quick JSON.parse(JSON.stringify(newItems[source.droppableId])) works great! Then when you’re ready to save the changes, go through the copy to get the new order and you should be good to go!

awesome…I am really enjoying learning Redwood and how it all works. I will work on that part of this now.

Thanks for your help here!!

1 Like

You’re welcome! :slight_smile: Let us know if you run into any other issues!

1 Like

A beautiful example of a very quick solution for a very smart RedwooJS “newbie”. Kudos to @rob

Here is working code:

import React, { useState } from 'react'

import { DragDropContext } from 'react-beautiful-dnd'

import { useMutation } from '@redwoodjs/web'

import PlanRow from '../PlanRow/PlanRow/PlanRow'

export const QUERY = gql`
  query PlanQuery {
    planRows {
      id
      order
      body
      age
      planItems {
        id
        order
        body
        status
        area
      }
    }
  }
`

const UPDATE_PLAN_ROW_MUTATION = gql`
  mutation UpdatePlanRowMutation($id: Int!, $input: UpdatePlanRowInput!) {
    updatePlanRow(id: $id, input: $input) {
      id
      order
    }
  }
`

const UPDATE_PLAN_ITEM_MUTATION = gql`
  mutation UpdatePlanItemMutation($id: Int!, $input: UpdatePlanItemInput!) {
    updatePlanItem(id: $id, input: $input) {
      id
      order
    }
  }
`

export const Loading = () => <div>Loading...</div>

export const Empty = () => <div>Empty</div>

export const Failure = ({ error }) => (
  <div style={{ color: 'red' }}>Error: {error?.message}</div>
)

export const Success = ({ planRows }) => {
  const [updatePlanRow] = useMutation(UPDATE_PLAN_ROW_MUTATION)
  const [updatePlanItem] = useMutation(UPDATE_PLAN_ITEM_MUTATION)

  let planData = { plans: [] }
  const [items, setItems] = useState({ ...planData, plans: planRows })

  function onDragEnd(props) {
    const { source, destination, type } = props
    if (!destination) {
      return
    }

    const sourceIndex = parseInt(source.index)
    const destIndex = destination.index
    const sourceParentId = parseInt(source.droppableId, 0)
    const destParentId = parseInt(destination.droppableId, 0)

    if (type === 'planItem') {
      const newItems = JSON.parse(JSON.stringify(items))

      const item = newItems[source.droppableId][sourceIndex]

      newItems[source.droppableId].splice(sourceIndex, 1)
      newItems[destination.droppableId].splice(destIndex, 0, item)

      newItems.plans.map((item, index) => {
        updatePlanRow({
          variables: { id: item.id, input: { order: index + 1 } },
        })
      })

      return setItems(newItems)
    }

    if (sourceParentId === destParentId) {
      const newItems = JSON.parse(JSON.stringify(items))
      const plan = type.split('-')[0]

      const planItem = newItems[plan]
        .map((item) => item.id)
        .indexOf(sourceParentId)
      const item = newItems[plan][planItem].planItems[sourceIndex]

      newItems[plan][planItem].planItems.splice(sourceIndex, 1)
      newItems[plan][planItem].planItems.splice(destIndex, 0, item)

      newItems.plans[planItem].planItems.map((pi, index) => {
        updatePlanItem({
          variables: { id: pi.id, input: { order: index + 1 } },
        })
      })
      return setItems(newItems)
    }
  }
  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div>
        {Object.keys(items).map((plan) => (
          <div key={plan.id}>
            <PlanRow data={items[plan]} type={plan} />
          </div>
        ))}
      </div>
    </DragDropContext>
  )
}