Reorder List Items With Drag-n-Drop in React + DynamoDB App
Published: 2023-02-06 03:07:28

✨ [**Watch on YouTube**](https://youtu.be/Nzd8DQznlaw)

Let's implement a list reordering feature in a React app backed by a DynamoDB database using [react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd). Here I have my productivity app [Increaser](https://increaser.org), where you can track daily habits for better sleep, mood or productivity. Recently some users asked for a drag-n-drop support to organize habit items by priority.

![](list.png)

## Order in DynamoDB

I store habits as an attribute of the user table item. Instead of using a list, I keep them as a record with the `id` as a key.

```tsx
export type User = {
  // ...
  habits: Record<string, Habit>
}
```

That way, when the user changes a single habit in the app, we only need to update that specific record element rather than changing the whole list and causing a more expensive operation.

```tsx
export const putHabit = async (userId: string, habit: Habit) => {
  await documentClient
    .update({
      ...getUserItemParams(userId),
      UpdateExpression: `set habits.#id = :habit`,
      ExpressionAttributeValues: {
        ":habit": habit,
      },
      ExpressionAttributeNames: {
        "#id": habit.id,
      },
    })
    .promise()
}
```

When the user drags the habit to a new position, we update only a single habit by changing its `order` attribute. This attribute doesn't have any meaning in a context of a single item and is only used to sort habits between each other. And here comes the front-end function we use to get a new habit order after moving an item from a position with the `sourceIndex` to a new one with `destinationIndex`.

```tsx
export const getNewOrder = (
  orders: number[],
  sourceIndex: number,
  destinationIndex: number
): number => {
  const sourceOrder = orders[sourceIndex]
  if (orders.length === 1) {
    return sourceOrder
  }

  if (destinationIndex === 0) {
    return orders[0] - 1
  }

  if (destinationIndex === orders.length - 1) {
    return orders[destinationIndex] + 1
  }
  if (destinationIndex < sourceIndex) {
    return (
      orders[destinationIndex - 1] +
      (orders[destinationIndex] - orders[destinationIndex - 1]) / 2
    )
  }

  return (
    orders[destinationIndex] +
    (orders[destinationIndex + 1] - orders[destinationIndex]) / 2
  )
}
```

When we move the item to the first position, we assign its order to the smallest one minus one, and we apply a similar logic when moving to the end of the list. Since the order is a floating number, we can always increase it by half a distance between the neighboring items.

## Moving Items with React-Beautiful-DnD

While there are other libraries for dragging elements, I've already used react-beautiful-dnd at a job, and it's designed specifically for moving items between lists.

```tsx
import { useUpdateHabitMutation } from "habits/api/useUpdateHabitMutation"
import { useCallback } from "react"
import {
  DragDropContext,
  Droppable,
  OnDragEndResponder,
} from "react-beautiful-dnd"
import { getNewOrder } from "shared/utils/getNewOrder"
import styled from "styled-components"

import { CurrentHabitProvider } from "../CurrentHabitProvider"
import { useHabits } from "../HabitsProvider"
import { ActiveHabit } from "./ActiveHabit"

const Container = styled.div`
  > * {
    margin-top: 16px;
  }
`

export const ActiveHabitsList = () => {
  const { habits } = useHabits()

  const { mutate: updateHabit } = useUpdateHabitMutation()
  const handleDragEnd: OnDragEndResponder = useCallback(
    ({ destination, source, draggableId }) => {
      if (!destination) {
        return
      }

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

      const order = getNewOrder(
        habits.map(({ order }) => order),
        source.index,
        destination.index
      )

      updateHabit({
        id: draggableId,
        order,
      })
    },
    [habits, updateHabit]
  )

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId="habits">
        {(provided) => (
          <Container ref={provided.innerRef} {...provided.droppableProps}>
            {habits.map((habit) => (
              <CurrentHabitProvider value={habit} key={habit.id}>
                <ActiveHabit />
              </CurrentHabitProvider>
            ))}
            {provided.placeholder}
          </Container>
        )}
      </Droppable>
    </DragDropContext>
  )
}
```

We wrap the list with `DragDropContext` and provide callbacks. In the habit list, we only want to know about the drag end so we can call the API and update the list. Then we render `Droppable` and container for the habits. While we always use flexbox's gap attribute to create a space between items, the library doesn't support it, so we had to use margins.

```tsx
import { Draggable } from "react-beautiful-dnd"
import styled, { css } from "styled-components"
import { defaultBorderRadiusCSS } from "ui/borderRadius"
import { HStack, VStack } from "ui/Stack"
import { Text } from "ui/Text"
import { EmojiTextPrefix } from "ui/Text/EmojiTextPrefix"

import { useCurrentHabit } from "../CurrentHabitProvider"
import { useHabits } from "../HabitsProvider"
import { useActiveHabits } from "./ActiveHabitsContext"
import { HabitAnalytics } from "./HabitAnalytics"
import { HabitProgress } from "./HabitProgress"
import { ManageHabit } from "./ManageHabit"

const Card = styled.div<{ isDragging?: boolean }>`
  ${defaultBorderRadiusCSS}
  background-color: ${({ theme }) => theme.colors.background.toCssValue()};
  padding: 20px;
  ${({ isDragging }) =>
    isDragging &&
    css`
      transform: rotate(4deg);
    `}
`

export const ActiveHabit = () => {
  const habit = useCurrentHabit()
  const { emoji, name, id } = habit
  const { isReadonly } = useActiveHabits()

  const { habits } = useHabits()

  const index = habits.indexOf(habit)

  const content = <div>habit content...</div>

  return (
    <Draggable index={index} draggableId={id}>
      {(provided, { isDragging }) => (
        <div
          ref={provided.innerRef}
          {...provided.draggableProps}
          {...provided.dragHandleProps}
        >
          <Card isDragging={isDragging}>{content}</Card>
        </div>
      )}
    </Draggable>
  )
}
```

We wrap the habit item with a Draggable component with two required parameters - `id` and `index`. Since we store the habits in the context, we don't need to provide the index as a property and get with the `indexOf` method. When dragging the item, we slightly rotate the card for a better UX. It's possible to show a customized placeholder like a shadow under the dragged element, yet it requires some extra work since react-beautiful-dnd doesn't provide such customization, and we have only a transparent placeholder.