How to Implement Drag and Drop Image With File Upload in Laravel and React

September 24, 2024
Written by
Lucky Opuama
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Implement Drag and Drop Image With File Upload in Laravel and React

File and image uploads are essential functionality for many applications, ensuring a smooth and user-friendly experience. Whether you are creating a photo-sharing website, a document management system, or any other application that works with dynamically generated content, having an intuitive file upload mechanism is essential. By integrating drag-and-drop functionality, users can easily upload files by dragging them from their file explorer and dropping them onto a designated area.

In this tutorial, you'll learn how to implement drag-and-drop file upload functionality in Laravel with React.

Prerequisites

To follow through with this tutorial, ensure you have the following:

Create a new Laravel project

To create a new Laravel project and change into the new project directory, run the following commands.

composer create-project --prefer-dist laravel/laravel file_upload
cd file_upload

Then, start the development server by running the following command in your terminal:

php artisan serve

To confirm that the Laravel application is running correctly, open http://127.0.0.1:8000 in your preferred browser. You should see a page that matches the screenshot below.

Set up the database

Why SQLite

The latest version of Laravel (version 11) uses SQLite as the default database. SQLite is a strong, high-performance, and user-friendly database solution, designed for applications that demand a small, fast, and self-contained database, one that does not require a separate server.

Its simplicity and adaptability make it a popular choice for mobile apps, desktop software, and embedded systems. SQLite's ease of use and efficient performance make it ideal for a wide range of use cases, from tiny projects to more complicated applications.

Create a database migration file

To create a new migration file in the database/migrations directory, run the following command in your terminal:

php artisan make:migration create_uploads_table

Next, navigate to database/migrations, open the file ending with _create_uploads_table.php, and update the up() function to match the following code:

public function up()
{
    Schema::create('uploads', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('message');
        $table->string('file_name');
        $table->timestamps();
    });
}

This code defines the structure of the uploads table in the database.

Now, to create the uploads table in your database with the structure specified above, run the following command:

php artisan migrate

Congratulations! You have successfully created your database and the necessary structure for this project.

Create a database model

Now, let's provide a programmatic means of interacting with the table, and ensure that only specific fields can be mass-assigned. This is where the model comes in.

To create the model, run the following command:

php artisan make:model Upload

Next, navigate to app/Models directory, open the Upload.php file, and update it with the following code:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Upload extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
        'message',
        'file_name'
    ];
}

Each instance of the Upload model corresponds to a record in the uploads table.

Create the upload controller

Next, you need to generate a controller to handle file uploads. To do that, run the command below:

php artisan make:controller UploadController

Then, navigate to app/Http/Controllers and open the UploadController.php file. Replace the existing code with the following and save the changes:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Upload;

class UploadController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'message' => 'required|string',
            'file' => 'required|mimes:jpeg,png,gif,pdf,doc,docx|max:5000'
        ]);

        if ($request->hasFile('file')) {
            $file = $request->file('file');
            $filePath = $file->store('uploads');
            $upload = Upload::create([
                'name' => $request->input('name'),
                'message' => $request->input('message'),
                'file_name' => $file->getClientOriginalName(),
            ]);

            return response()->json(['upload' => $upload], 201);
        }

        return response()->json(['error' => 'File not uploaded'], 400);
    }
}

The code above validates the incoming request, processes the file upload, saves the relevant data to the database, and returns a JSON response based on whether the upload was successful or not.

Run the command below to create an API route that will handle the file upload.

php artisan install:api

Next, Open the routes/api.php file and add the code below:

Route::post('/upload-endpoint', [App\Http\Controllers\UploadController::class, 'store']);

This route sets up a POST endpoint in your project that directs incoming requests to the /upload-endpoint URL path, invoking the store() method defined within the UploadController class.

Configure Laravel's file storage

Laravel uses the storage/app directory for file storage, by default. Run the following command to create a symbolic link from the storage/app/public directory to the public/storage directory. This makes the files accessible within the application.

php artisan storage:link

Configure Cross-Origin Resource Sharing

Cross-Origin Resource Sharing (CORS) is a security feature implemented by web browsers to safeguard against web pages making requests to domains other than the one that served the original web page. In a nutshell, CORS is a security feature that prevents unauthorized access.

Laravel 10 and later versions have built-in support for handling CORS via its configuration file, making it easier to manage CORS settings without needing an external package.

To publish the CORS configuration file run the following command:

php artisan config:publish cors

Now, open the config/cors.php and update the configuration with the following code to customize your CORS settings:

<?php
return [
    'paths' => ['api/*', 'upload-endpoint', 'sanctum/csrf-cookie'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['http://localhost:5173'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => [],
    'max_age' => 0,
    'supports_credentials' => false,
];

The code above configures the following specific settings:

  • paths: Specifies the paths (api/*, upload-endpoint, sanctum/csrf-cookie) that should be CORS-enabled. Requests made to these paths will be subject to CORS policies.
  • allowed_origins: Specifies the origins/domains (http://localhost:5173) that are allowed ate a new React project

Now, let's shift our focus to the frontend. Start by creating a new React/Vite template running the command below:

npm create vite@latest my-react-app --template react

You’ll be presented with a list of frameworks. Use the arrow keys to navigate and select React.

Lastly, you’ll be prompted to choose a variant for your application. For this tutorial select Javascript.

Now, navigate to the my-react-app directory by running the command below:

cd my-react-app

Next, run the following command to install the required packages.

npm install axios react-dropzone
  • Axios is a popular JavaScript library used for making HTTP requests from the browser and Node.js. It is based on the promise API, making it easier to handle asynchronous operations.
  • Dropzone is a JavaScript library that provides a user-friendly interface for handling file uploads with features like drag-and-drop functionality, image previews, and more.

Create the template

Now, navigate to the src directory inside your my-react-app directory. Create a new folder named components. Inside the components folder, create a new file called MediaLibraryAttachment.jsx and paste the following code into it:

import React, { useCallback, useState } from "react";
import { useDropzone } from "react-dropzone";

const MediaLibraryAttachment = ({
    name,
    initialValue,
    validationRules,
    validationErrors,
    onFileChange,
}) => {
    const [file, setFile] = useState(initialValue);
    const onDrop = useCallback(
        (acceptedFiles) => {
            setFile(acceptedFiles[0]);
            onFileChange(acceptedFiles[0]);
        },
        [onFileChange]
    );
    const { getRootProps, getInputProps } = useDropzone({
        onDrop,
        accept: validationRules.accept.join(", "),
        maxSize: validationRules.maxSizeInKB * 1024,
        multiple: false,
    });
    return (
        <div {...getRootProps({ className: "dropzone" })}>
            <input {...getInputProps()} name={name} />
            <p>Drag 'n' drop a file here, or click to select a file</p>
            {file && <p>Selected file: {file.name}</p>}
            {validationErrors && <span>{validationErrors}</span>}
        </div>
    );
};

export default MediaLibraryAttachment;

The MediaLibraryAttachment component offers a drag-and-drop file upload interface, utilizing the react-dropzone library to manage file selection, validation, and user feedback.

Now, navigate back to the my-react-app/src directory and create a new file named MyForm.jsx. This file will house the main form component, which serves as the user interface for uploading files and additional information, such as the user's name and message. This component provides a streamlined user experience for submitting data to the Laravel backend for processing and storage.

Add the following code to the file to set up the form:

import React, { useState } from "react";
import MediaLibraryAttachment from "./components/MediaLibraryAttachment";
import "./index.css";

const MyForm = () => {
    const [file, setFile] = useState(null);
    const [validationErrors, setValidationErrors] = useState(null);
    const [responseMessage, setResponseMessage] = useState("");
    const [name, setName] = useState("");
    const [message, setMessage] = useState("");
    const handleFileChange = (file) => {
        setFile(file);
    };
    const handleSubmit = async (event) => {
        event.preventDefault();
        const formData = new FormData();
        formData.append("name", name);
        formData.append("message", message);
        if (file) {
            formData.append("file", file);
        }
        try {
            const response = await fetch(
                "http://127.0.0.1:8000/api/upload-endpoint",
                {
                    method: "POST",
                    body: formData,
                }
            );
            if (!response.ok) {
                const errorData = await response.json();
                throw new Error(
                    errorData.message || "Network response was not ok"
                );
            }
            const responseData = await response.json();
            console.log("Upload successful", responseData);
            setResponseMessage("Upload successful");
            setTimeout(() => {
                window.location.reload();
            }, 3000);
        } catch (error) {
            console.error("Upload error", error);
            setValidationErrors({
                general: error.message,
            });
        }
    };
    return (
        <div className="container">
            <div className="text-container">
                <h2>
                    Drag and Drop File Upload <br />
                    in Laravel with React
                </h2>
                <p>Please fill out the form and upload your file.</p>
            </div>
            <form className="form-container" onSubmit={handleSubmit}>
                {responseMessage && (
                    <p className="message">{responseMessage}</p>
                )}
                <div>
                    <label htmlFor="name">Name:</label>
                    <input
                        type="text"
                        id="name"
                        name="name"
                        value={name}
                        onChange={(e) => setName(e.target.value)}
                    />
                </div>
                <div>
                    <label htmlFor="message">Message:</label>
                    <textarea
                        id="message"
                        name="message"
                        value={message}
                        onChange={(e) => setMessage(e.target.value)}
                    />
                </div>
                <label htmlFor="upload">Upload a File:</label>
                <MediaLibraryAttachment
                    name="file"
                    initialValue={file}
                    validationRules={{
                        accept: [
                            "image/png",
                            "image/jpeg",
                            "image/gif",
                            "application/pdf",
                            "application/msword",
                            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                        ],
                        maxSizeInKB: 5000,
                    }}
                    validationErrors={validationErrors?.file}
                    onFileChange={handleFileChange}
                />
                <button type="submit">Submit</button>
                {validationErrors?.general && (
                    <div>{validationErrors.general}</div>
                )}
                {validationErrors && (
                    <div>
                        <ul>
                            {Object.keys(validationErrors).map((key) => (
                                <li key={key}>{validationErrors[key]}</li>
                            ))}
                        </ul>
                    </div>
                )}
            </form>
        </div>
    );
};

export default MyForm;

This code creates a form that allows users to upload a file along with their name and message. The form features a drag-and-drop interface for file uploads, utilizing the MediaLibraryAttachment component.

Next, open the my-react-app/src/main.jsx file to update the main entry point, updating the file to match the code below:

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import MyForm from "./MyForm";

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <MyForm />
    </React.StrictMode>
);

Now, ensure your application is well-structured and renders efficiently in your browser by updating my-react-app/src/index.css with the following code:

body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

.container {
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: #df2424;
    background: white;
    padding: 2rem;
    margin-left: 15rem;
    margin-top: 100px;
    width: 50rem;
    border-radius: 25px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.text-container h2 {
    color: #007bff;
    font-size: 2rem;
}

.form-container {
    padding: 2rem;
    width: 30rem;
    border-radius: 8px;
}

.form-container div {
    margin-bottom: 1rem;
}

.form-container .message {
    color: green;
    margin-left: 10rem;
}

.form-container label {
    display: block;
    margin-bottom: 0.5rem;
}

.form-container input[type="text"],
.form-container textarea {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ccc;
    border-radius: 4px;
}

.form-container button {
    display: inline-block;
    padding: 0.75rem 1.5rem;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
}

.form-container button:hover {
    background-color: #0056b3;
}

.dropzone {
    border: 2px dashed #cccccc;
    padding: 20px;
    text-align: center;
    cursor: pointer;
}

.dropzone p {
    margin: 0;
}

.dropzone span {
    color: red;
}

Test that the application works

With the application built, it's time to test it. So, if you stopped the Laravel application, restart it by running the following command in the project's top-level directory:

php artisan serve

Next, in a new terminal session or tab, navigate to the my-react-app directory and start your React frontend application by running the following command:

npm run dev

Now, verify that the new functionality works correctly by opening http://localhost:5173 in your preferred web browser. Fill out the form, utilize the drag-and-drop feature to upload a file, and then submit the form as demonstrated in the short clip below.

After uploading several images, the uploads table in your database should look like the screenshot below:

And, if you look in the storage/app/uploads directory, you'll find the images that you uploaded.

That's how to implement drag and drop image with file upload in Laravel with React

With this tutorial, you have built a single-page, full-stack application using React.js, with Vite.js on the frontend and Laravel on the backend, to implement a drag-and-drop file upload application.

You have learned how to:

  • Set up the backend: You configured Laravel to handle file uploads, including setting up routes and controllers, and implementing validation rules.
  • Design the frontend: You used React.js to create a seamless drag-and-drop interface, leveraging libraries like react-dropzone to handle file selection and upload.
  • Integrate backend and frontend: You connected your React frontend to the Laravel backend using API requests to handle file uploads efficiently.

Now, you know how to manually integrate and manage React.js in your Laravel app, allowing you to develop modern and interactive web applications with ease. Happy coding!

Lucky Opuama is a software engineer and technical writer with a passion for exploring new tech stacks and writing about them. Connect with him on LinkedIn.

The upload icon in the post image was created by Ilham Fitrotul Hayat on Flaticon.