tutorial
13 min read
5/2/2024
Create Drag-and-Drop List Reordering Feature in Next.js 14: A Step-by-Step Guide
Learn how to effortlessly implement a drag-and-drop feature for reordering lists in Next.js. Enhance your web application's interactivity and user experience with step-by-step instructions.
nextjs
typescript
shadcn-ui
tailwindcss
tailwind
shadcn
next
react
reactjs
## Next.js Installation
Let's kick things off by setting up a brand new Next.js project.
```bash title="terminal"
npx create-next-app@latest my-app
```
During this setup, you'll be prompted to make some choices:
```css title="terminal"
What is your project named? my-app /* Choose a descriptive name for your project (e.g., "drag-and-drop-list") */
Would you like to use TypeScript? No / Yes /* For enhanced type safety and future-proofing, consider using TypeScript (select "Yes") */
Would you like to use ESLint? No / Yes /* Using ESLint (select "Yes") will help maintain code quality and consistency */
Would you like to use Tailwind CSS? No / Yes /* Tailwind CSS (select "Yes") offers a great utility-first approach to styling, making it easy to create responsive layouts */
Would you like to use `src/` directory? No / Yes /* Stick with the default `src` directory (select "No") */
Would you like to use App Router? (recommended) No / Yes /* For a modern routing experience, choose "Yes" for App Router */
Would you like to customize the default import alias (@/*)? No / Yes /* You can customize import paths later, so "No" is fine for now */
```
Once you've made your selections, `create-next-app` will create your project directory and install the necessary dependencies.
## Running the Application
In your terminal, navigate to the project's root directory:
```bash title="terminal"
cd my-app
```
Start the development server:
```bash title="terminal"
npm run dev
```
Open `http://localhost:3000` in your web browser to view the Next.js application.
## Preparing the Playground (Clear Canvas!)
Now, let's navigate to the `app/page.tsx` file, which is the main component of your Next.js application. We'll clear out any existing code to make room for our drag-and-drop magic:
1. Open `app/page.tsx` in your preferred code editor.
2. Delete the code that's present.
Here's the cleaned-up `app/page.tsx` file for reference:
```tsx title="page.tsx"
// app/page.tsx
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center gap-4 p-24">
<h1 className="text-xl font-medium">
Drag-and-Drop List Reordering Feature
</h1>
</main>
);
}
```
Next, head over to the `app/globals.css` file. This file holds global styles that apply throughout your application. We want to keep it focused on Tailwind CSS essentials:
```css title="globals.css"
/* app/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
```
## Introducing the Drag-and-Drop Hero (Library Time!)
To empower our component with drag-and-drop functionality, we'll leverage a fantastic library called `@hello-pangea/dnd`. It provides smooth, accessible drag-and-drop interactions for React lists. Install it using npm:
```bash title="terminal"
npm install @hello-pangea/dnd --save
```
## Component Organization (Keep It Clean!)
To maintain a well-structured project, let's create a dedicated `components` folder within the project's root directory. Inside this folder, create a new file named `drag-and-drop.tsx`. This file will house the core logic for our drag-and-drop component:
```
š my-app
ā š app
ā š components
ā ā š drag-and-drop.tsx
... (other files and folders)
```
## Building the Drag-and-Drop Core (Step-by-Step Breakdown!)
1. **File Header and Client-Side Boundary:** Start by adding the following lines to the top of `drag-and-drop.tsx`:
```tsx title="drag-and-drop.tsx"
"use client";
import { useState } from "react";
```
- `"use client"` tells Next.js that this component's code should only run on the client-side (the user's browser) because it interacts with the DOM.
- `useState` is a React hook for managing state within the component.
2. **Sample Data (Mimicking a Database):** Let's create some sample data that simulates what might come from a database. This data represents chapters in a blog post:
```tsx title="drag-and-drop.tsx"
const data = [
{
id: "id-1",
title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor",
position: 1,
},
{
id: "id-2",
title: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore",
position: 2,
},
{
id: "id-3",
title: "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt",
position: 3,
},
];
```
3. **Interface Definition (Shaping the Data):**
```tsx title="drag-and-drop.tsx"
interface Chapter {
id: string;
title: string;
position: number;
}
```
This defines an interface called `Chapter`, which specifies the structure of each chapter object in our data. This helps with type safety and improves code readability.
4. **Component Function and State Management:**
```tsx title="drag-and-drop.tsx"
export function DragAndDrop() {
const [chapters, setChapters] = useState<Chapter[]>(data);
// ... rest of the component logic
}
```
- `export function DragAndDrop()` defines the `DragAndDrop` component.
- `const [chapters, setChapters] = useState<Chapter[]>(data);` initializes the state variable `chapters` using the `useState` hook. It holds the array of chapter objects using the `Chapter` interface for type safety. The initial value is set to the `data` array we defined earlier.
## Importing the Drag-and-Drop Powerhouse (Connecting the Library)
Now, let's import the necessary components from the `@hello-pangea/dnd` library and incorporate them into our `DragAndDrop` component:
```tsx title="drag-and-drop.tsx"
import { useState } from "react";
import {
DragDropContext,
Droppable,
Draggable,
type DropResult,
} from "@hello-pangea/dnd";
```
These imports provide the building blocks for drag-and-drop functionality:
- `DragDropContext`: Wraps the entire draggable area and handles the overall drag-and-drop logic.
- `Droppable`: Defines a droppable zone where draggable items can be placed.
- `Draggable`: Makes individual items draggable within the droppable zone.
- `type DropResult`: Provides the type information for the result of a drag-and-drop operation.
## Handling Drag End (Reordering the Chapters)
We'll create a function called `onDragEnd` that handles the conclusion of a drag operation:
```tsx title="drag-and-drop.tsx"
const onDragEnd = (result: DropResult) => {
if (!result.destination) return; // Early exit if not dropped in a valid area
const items = chapters; // Make a copy of the chapters array to avoid mutation
const [reorderedItem] = items.splice(result.source.index, 1); // Remove item from source
items.splice(result.destination.index, 0, reorderedItem); // Insert item at destination
setChapters(items); // Update the state with the reordered chapters
};
```
This function:
1. Checks if the item was dropped in a valid location (`result.destination`). If not, it exits.
2. Creates a copy of the `chapters` array to avoid directly mutating the state (a best practice).
3. Extracts the item that was dragged (`reorderedItem`) from its original position.
4. Inserts the `reorderedItem` at its new position in the copied array.
5. Updates the component's state with the reordered chapters using `setChapters()`.
## Component Structure (Putting It All Together)
Now, let's assemble the core logic of the `DragAndDrop` component using the imported components and the `onDragEnd` function:
```tsx title="drag-and-drop.tsx"
export function DragAndDrop() {
const [chapters, setChapters] = useState<Chapter[]>(data);
const onDragEnd = (result: DropResult) => {
// Drag end handling logic
};
return (
<DragDropContext onDragEnd={onDragEnd}>
{/* Draggable area and chapters */}
</DragDropContext>
);
}
```
This structure:
1. Wraps the draggable area with `DragDropContext`, passing `onDragEnd` as a prop.
2. Inside `DragDropContext`, we'll add the necessary components to define the draggable and droppable elements (coming up next).
## Creating the Droppable Zone (Where the Chapters Land)
1. Inside the `DragDropContext` in `DragAndDrop.tsx`, we'll use the `Droppable` component to define a droppable area where chapters can be placed:
```tsx title="drag-and-drop.tsx"
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="chapters">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{/* Draggable chapters go here */}
</div>
)}
</Droppable>
</DragDropContext>
```
- `Droppable droppableId="chapters"` creates a droppable zone with the ID "chapters." This ID is used for associating draggable items with their droppable target.
- The function provided to `Droppable` renders a `div` that acts as the container for the draggable chapters.
- `provided.droppableProps` and `ref={provided.innerRef}` are props passed by the `Droppable` component for proper integration with the library.
## Making Chapters Draggable (The Fun Part)
2. Now, let's iterate through the `chapters` array and create `Draggable` components for each chapter inside the `Droppable` zone:
```tsx title="drag-and-drop.tsx"
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="chapters">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{chapters.map((chapter, index) => (
<Draggable key={chapter.id} draggableId={chapter.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
className="..." /* Styling later */
>
{/* Chapter content */}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
```
- We use `chapters.map` to iterate through each chapter in the state and create a `Draggable` component for each.
- `key={chapter.id}` is essential for React to efficiently identify and update individual chapters.
- `draggableId={chapter.id}` associates the draggable item with the droppable zone using the same ID ("chapters").
- `index={index}` provides the current index of the chapter within the array, which is crucial for reordering.
- The function provided to `Draggable` renders another `div` that represents the draggable chapter itself. We'll add styling in the next step.
3. The `provided.innerRef` and `provided.draggableProps` are again passed down for proper drag-and-drop functionality.
## Adding Chapter Content (Populating the List)
Inside the `DragAndDrop` component (`drag-and-drop.tsx`), within the `chapters.map` function, you can now render the actual content of each chapter:
```tsx title="drag-and-drop.tsx"
{
chapters.map((chapter, index) => (
<Draggable key={chapter.id} draggableId={chapter.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
className="..." /* Styling later */
>
<div className="..." {...provided.dragHandleProps}>
{/* Drag handle content */}
</div>
<p>
{chapter.position}. {chapter.title}
</p>
</div>
)}
</Draggable>
))
}
```
- Update the JSX within the `map` function to display the desired content.
- In this example, we render the chapter's position (`chapter.position`) followed by a dot (".") and then the chapter's title (`chapter.title`).
## Integrating the Drag-and-Drop Component (Bringing It All Together)
Now that we have the `DragAndDrop` component with its core functionality, let's use it in our main application file, `app/page.tsx`.
**Importing and Rendering the Drag-and-Drop:**
1. Import the `DragAndDrop` component from `components/drag-and-drop.tsx` into `app/page.tsx`:
```tsx title="page.tsx"
import { DragAndDrop } from "@/components/drag-and-drop";
```
2. Render the `DragAndDrop` component within the JSX of `app/page.tsx`:
```tsx title="page.tsx"
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center gap-4 p-24">
<h1 className="text-xl font-medium">Drag-and-Drop List Reordering</h1>
<DragAndDrop />
</main>
);
}
```
With this integration, the `DragAndDrop` component will now render within your Next.js application, providing the drag-and-drop functionality to reorder the chapter list.
## Styling the Drag-and-Drop List (Visual Polish)
Now that we've built the core functionality of the drag-and-drop component in `drag-and-drop.tsx`, let's enhance its visual presentation with some styling.
**Adding a Drag Handle Styles**
Inside the `Draggable` component, we can create a dedicated visual element for users to grab and drag the chapter:
"Let's start by adding the `lucide-react` library, from which we will import the `Grab` icon."
```bash title="terminal"
npm install lucide-react
```
At the top of the `drag-and-drop.tsx` component, import the icon from `lucide-react`.
```tsx title="drag-and-drop.tsx"
"use client";
import { useState } from "react";
import {
DragDropContext,
Droppable,
Draggable,
type DropResult,
} from "@hello-pangea/dnd";
import { Grip } from "lucide-react";
```
Next, add the icon and styles to the `{/* Drag handle content */}` `div`. This will be the visual element users can grab to drag the chapter.
```tsx title="drag-and-drop.tsx"
<Draggable key={chapter.id} draggableId={chapter.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
className="..." /* Styling later */
>
<div
className="px-2 py-3 border-r hover:bg-muted rounded-l-md transition cursor-grab"
{...provided.dragHandleProps}
>
<Grip className="h-5 w-5" />
</div>
<p>
{chapter.position}. {chapter.title}
</p>
</div>
)}
</Draggable>
```
This styling provides a basic visual cue for the draggable area. You can customize it further using colors, icons, or borders to match your application's design.
## Styling the Draggable Chapters
Apply styles to the `div` that represents the draggable chapter itself:
```tsx title="drag-and-drop.tsx"
<Draggable key={chapter.id} draggableId={chapter.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
className="flex items-center gap-x-2 border rounded-md mb-4 text-sm"
>
<div
className="px-2 py-3 border-r hover:bg-neutral-100 rounded-l-md transition cursor-grab"
{...provided.dragHandleProps}
>
<Grip className="h-5 w-5" />
</div>
<p>
{chapter.position}. {chapter.title}
</p>
</div>
)}
</Draggable>
```
This styling creates a more visually appealing list item with padding, and rounded corners. You can customize it further to match your application's design language.
## Additional Considerations
- You can modify the initial `data` array in `drag-and-drop.tsx` to include more chapters or adjust their properties (`id`, `title`, `position`).
- Consider error handling and validation for user input if you plan to allow dynamic modification of the chapter data.
- Explore more advanced features provided by the `@hello-pangea/dnd` library, such as custom drag images or visual feedback during drag operations.
With these steps, you've successfully created a functional Drag-and-Drop list component for your Next.js application! Feel free to customize the styling and chapter content to fit your specific needs.
Loading...