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.

Article Thumbnail
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.
Author
Loading...
Ā©DEVCreated