photo

Giới thiệu

Xin chào các bạn!
Như các bạn đã biết, React là một thư viện JavaScript, dùng để xây dựng giao diện người dùng một cách linh hoạt. Nó cho phép bạn soạn các giao diện người dùng phức tạp từ các đoạn mã nhỏ và biệt lập được gọi là “components”. Chúng ta sử dụng các components để khai báo cho React biết những gì hiển thị trên màn hình (buttons, forms,…) . Khi dữ liệu thay đổi, React sẽ cập nhật và re-render các components đó.

Trong bài viết này mình sẽ hướng dẫn các bạn tạo một ứng dụng ImageUploader đơn giản được viết bằng React.

Ứng dụng này cho phép người dùng kéo, thả hình ảnh vào màn hình hoặc Upload từ file img sau đó hiển thị các hình ảnh đó dưới dạng masonry (kiểu bố cục tường gạch) và cho phép xem chi tiết từng hình ảnh, xóa và đổi tên ảnh.
Mình sẽ bỏ qua phần tạo ReactJS project.
Demo: https://app-photo-gallery.vercel.app/
Hãy cùng mình đi sâu vào quy trình từng bước để xây dựng ứng dụng này.

Bước 1. Cài đặt các thư viện :

npm i react-dropzone
npm i react-responsive-masonry

Bước 2: Tạo một tệp mới có tên ImageUploader trong thư mục src và Import các thành phần

import React, { useState, useEffect, useRef } from "react";
import { useDropzone } from "react-dropzone";
import Masonry, { ResponsiveMasonry } from "react-responsive-masonry";
ImageUploader.jsx

Bước 3: Trong hàm ImageUploader khai báo các state

const ImageUploader = () => {
   // Khởi tạo State, ban đầu chưa có ảnh thì mặc định là null
  const [images, setImages] = useState([]);
  const [newImage, setNewImage] = useState(null);
   // Các thành phần tham chiếu để xử lý việc kéo thả tệp
  const dropzoneRef = useRef(null);
  const newImageRef = useRef(null);
  // tạo state để cập nhật giá trị của data khi người dùng chọn một hình ảnh để xem hoặc chuyển đổi giữa các hình ảnh khác nhau. 
   const [data, setData] = useState({ img: "", i: 0 });
   return(
            ...
         )
   }
  export default ImageUploader;
ImageUploader.jsx

Bước 4: Cập nhật (xử lý sự kiện)

  • Khai báo hàm onDrop để xử lý sự kiện khi người dùng thả các tệp tin hình ảnh vào khu vực dropzone. Hàm này gọi hàm handleDrop với tham số là danh sách các tệp tin hình ảnh đã được chấp nhận.
  const onDrop = (acceptedFiles) => {
    handleDrop(acceptedFiles);
  };
  // Gọi hook useDropzone và lưu giữ các giá trị trả về vào các biến getRootProps, getInputProps và isDragActive.
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: "image/*",
    multiple: true,
  });
ImageUploader.jsx
  • useEffect sử dụng để lấy danh sách hình ảnh từ localStorage khi component được render
const ImageUploader = () => {
  useEffect(() => {
       const storedImages = JSON.parse(localStorage.getItem("images"));
  if (storedImages) {
       setImages(storedImages);
  }
}, []);
return(
          ...
       ) }
export default ImageUploader;
ImageUploader.jsx
  • handleDrop xử lý khi có hình ảnh được thả vào khu vực dropzone.
const ImageUploader = () => {
 const handleDrop = (acceptedFiles) => {
     // Đọc từng file hình ảnh được tải lên và chuyển đổi thành dữ liệu base64 tạo một đối tượng mới
  acceptedFiles.forEach((file) => {
    const reader = new FileReader();
    reader.onload = () => {
      const imageBase64 = reader.result;
      const newImage = {
        id: Date.now().toString(),
        title: `Image ${images.length + 1}`,
        dataURL: imageBase64,
        shake: true,
      };
       // Cập nhật trạng thái images bằng cách thêm đối tượng mới vào mảng prevImages và lưu trữ danh sách ảnh vào localStorage.
setImages((prevImages) => {
        const updatedImages = [...prevImages, newImage];
        localStorage.setItem("images", JSON.stringify(updatedImages));
        return updatedImages;
      });
      setNewImage(null);
       // Sử dụng setTimeout để cuộn đến ảnh mới và loại bỏ hiệu ứng rung sau một khoảng thời gian.
      setTimeout(() => {
        scrollToNewImage(newImage.id);
      }, 100);
      setTimeout(() => {
        setImages((prevImages) =>
          prevImages.map((image) =>
            image.id === newImage.id ? { ...image, shake: false } : image
          )
        );
      }, 1000);
    };
    reader.readAsDataURL(file);
  });
};

return(
          ...
       ) }
export default ImageUploader;
ImageUploader.jsx
  • Tiếp theo scrollToNewImage được sử dụng để cuộn đến ảnh mới được tải lên.
  • Tìm phần tử DOM tương ứng với imageId và sử dụng phương thức scrollIntoView để cuộn đến vị trí của nó.
 const scrollToNewImage = (imageId) => {
   const masonryContainer = newImageRef.current;
   if (masonryContainer) {
     const imageElement = masonryContainer.querySelector(`#image-${imageId}`);

     if (imageElement) {
       imageElement.scrollIntoView({
         behavior: "smooth",
         block: "center",
         inline: "center",
       });
     }
   }
 };

ImageUploader.jsx
  • Hàm handleEditImage được sử dụng để chỉnh sửa tiêu đề của một ảnh. Nó tạo ra một bản sao mới của mảng images, cập nhật tiêu đề của ảnh có id khớp và cập nhật trạng thái images vào localStorage
  const handleEditImage = (id, newTitle) => {
    const updatedImages = images.map((image) => {
      if (image.id === id) {
        return { ...image, title: newTitle };
      }
      return image;
    });
    setImages(updatedImages);
    localStorage.setItem("images", JSON.stringify(updatedImages));
  };
ImageUploader.jsx
  • Hàm handleDeleteImage được sử dụng để xóa một ảnh.
  const handleDeleteImage = (id) => {
    const updatedImages = images.filter((image) => image.id !== id);
    setImages(updatedImages);
    localStorage.setItem("images", JSON.stringify(updatedImages));
  };
ImageUploader.jsx
  • Hàm viewImage cập nhật giá trị của state data, dẫn đến việc hiển thị hình ảnh được chọn và cập nhật chỉ số của hình ảnh trong danh sách.
  • Hàm imgAction được sử dụng để thực hiện hành động trên ảnh (chuyển đến ảnh kế tiếp, ảnh trước đó hoặc đóng ảnh).
const viewImage = (img, i) => {
    setData({ img, i });
  };
  const imgAction = (action) => {
    let i = data.i;
    const numImages = images.length;
     // Nếu hành động là "next-img", chúng ta tăng chỉ số i lên 1 (với toán tử % để tuần hoàn qua danh sách ảnh). 
     // Nếu hành động là "previous-img", chúng ta giảm chỉ số i đi 1 (với toán tử % để tuần hoàn qua danh sách ảnh).
     // Cuối cùng không có hành động nào được chỉ định, chúng ta đặt i về -1 để đóng ảnh. Sau đó, chúng ta cập nhật trạng thái data với ảnh tương ứng theo chỉ số i.
    if (action === "next-img") {
      i = (i + 1) % numImages;
    } else if (action === "previous-img") {
      i = (i - 1 + numImages) % numImages;
    } else {
      i = -1; // Chỉ số -1 để đóng ảnh
    }

    setData({ img: i >= 0 ? images[i] : "", i: i });
  };

ImageUploader.jsx

Bước 5: render dữ liệu

  • Đây là một div chứa các thuộc tính được truyền xuống từ getRootProps của hook useDropzone. getRootProps trả về các props cần thiết để kết nối thành phần với chức năng kéo và thả tệp. Class dropzone sẽ được thêm vào để tạo hiệu ứng khi kéo thả.
return (
     <div
      {...getRootProps()}
      className={`dropzone ${isDragActive ? "active" : ""}`}  >
        ...
    </div>
)
ImageUploader.jsx
  • Tiếp theo tạo input dùng để chọn tệp ảnh. Các props được truyền từ getInputProps của hook useDropzone để kết nối chức năng chọn tệp.
<div
      {...getRootProps()}
      className={`dropzone ${isDragActive ? "active" : ""}`}
    >
      <input className="inputdrag" {...getInputProps()} />
</div>
ImageUploader.jsx
  • Phần div có className=“drop” được sử dụng để hiển thị thông báo cho người dùng về cách thêm ảnh. Nếu đang kéo thả, thông báo sẽ là “Drop the images here…”. Nếu không, thông báo sẽ là “Drag and drop images here, or click to select images”.
<div
      {...getRootProps()}
      className={`dropzone ${isDragActive ? "active" : ""}`}
    >
      <input className="inputdrag" {...getInputProps()} />
      <div className="drop">
        {isDragActive ? (
          <p>Drop the images here...</p>
        ) : (
          <p>Drag and drop images here, or click to select images</p>
        )}
      </div>
       ...
</div>
ImageUploader.jsx
  • Trong thẻ div cha có className={dropzone ${isDragActive ? "active" : ""}}
    tạo thẻ div className=“image-uploader” Đây là phần chứa danh sách hình ảnh đã tải lên và chức năng xem ảnh lớn khi người dùng nhấp vào một hình ảnh trong danh sách. onClick được sử dụng để ngăn chặn sự kiện click từ việc lan truyền đến các thành phần cha.
<div
      {...getRootProps()}
      className={`dropzone ${isDragActive ? "active" : ""}`}
    >
   ...
     <div
        className="image-uploader"
        onClick={(event) => event.stopPropagation()} >
           ...
           </div>  
</div>
ImageUploader.jsx
  • Trong thẻ div className=“image-uploader” thêm {data.img && (…)}: Đoạn mã này kiểm tra xem có dữ liệu ảnh trong state data hay không. Nếu có, nó sẽ hiển thị một phần tử div className="viewImage"cho phép người dùng xem ảnh lớn .
<div
     {...getRootProps()}
     className={`dropzone ${isDragActive ? "active" : ""}`}
   >
  ...
    <div
       className="image-uploader"
       onClick={(event) => event.stopPropagation()} >
              {data.img && (
         <div className="viewImage">
           <button
             className="button"
             onClick={() => imgAction()}
             style={{ position: "absolute", top: "30px", right: "30px" }}
           >
             <i className="bi bi-x"></i>
           </button>
           <button
             style={{ position: "absolute", left: "10%" }}
             className="button"
             onClick={() => imgAction("previous-img")}
           >
             <i className="bi bi-caret-left-fill"></i>
           </button>
           <img
             src={data.img.dataURL}
             style={{ width: "auto", maxWidth: "90%", maxHeight: "90%" }}
           />
           <button
             style={{ position: "absolute", right: "10%" }}
             className="button"
             onClick={() => imgAction("next-img")}
           >
             <i className="bi bi-caret-right-fill"></i>
           </button>
         </div>
       )}
          </div>  
</div>
ImageUploader.jsx
  • Cuối cùng thêm thẻ div có className=“masonrylist” sau {data.img && (…)}. Phần này là danh sách các hình ảnh đã được tải lên. Nó sử dụng một thư viện gọi là Masonry để hiển thị các hình ảnh theo kiểu bố cục xếp hình. Masonry được bọc trong một ResponsiveMasonry để tùy chỉnh số cột của danh sách hình ảnh dựa trên kích thước màn hình.
  <div ref={newImageRef} className="masonrylist">
          <ResponsiveMasonry
            columnsCountBreakPoints={{ 350: 1, 750: 2, 900: 4 }}
          >
            <Masonry ref={dropzoneRef}>
              {images.map((image, i) => (
                 //Mỗi hình ảnh trong danh sách images được thêm vào nếu thuộc tính shake của hình ảnh được đặt thành true, để tạo hiệu ứng rung lắc.
                <div
                  key={image.id}
                  id={`image-${image.id}`}
                  className={`image-card image ${image.shake ? "shake" : ""}`}
                >
                  <div className="card">
                    <img
                      src={image.dataURL}
                      alt={image.title}
                      onClick={() => viewImage(image, i)}
                    />

                    <ul className="card-social">
                      <li className="card-social__item i1">
                         // phần tử <input> để người dùng có thể chỉnh sửa tiêu đề của hình ảnh.
                        <input
                          className="input-title"
                          type="text"
                          value={image.title}
                          onChange={(e) => {
                            handleEditImage(image.id, e.target.value),
                              e.stopPropagation();
                          }}
                        />
                      </li>
                    </ul>
                    <button
                      className="delete"
                      onClick={(e) => handleDeleteImage(image.id)}
                    >
                      <i className="bi bi-trash3"></i>
                    </button>
                  </div>
                </div>
              ))}
            </Masonry>
          </ResponsiveMasonry>
        </div>
ImageUploader.jsx

Kết luận

Trên đây là hướng dẫn tạo ứng dụng ImageUploader đơn giản của mình.
Source code: https://github.com/ducanhle31/App-photo-gallery
Các bạn có nhận xét hay ý kiến đóng góp gì thì hãy để lại comment cho mình nha.
Cảm ơn các bạn đã theo dõi bài viết.