Skip to main content

Product CRUD: upload ảnh, preview và progress

Ảnh sản phẩm là một ví dụ rất tốt để nhìn rõ cách Inertia kết nối state UI ở React với validation và storage ở Laravel mà không cần tự xây một REST upload API riêng. Bản đồ UI Product CRUD có thumbnail ở Index và preview/progress ở Edit

Ta đang muốn đạt UX gì?

Ở màn hình Create/Edit:
  • người dùng chọn ảnh và thấy preview ngay, chưa cần upload;
  • chọn ảnh mới sẽ thay preview hiện tại;
  • có thể xóa ảnh hiện tại ở Edit;
  • submit khóa field để tránh double-submit;
  • hiển thị phần trăm upload từ form.progress;
  • có thể cancel() request dài;
  • lỗi image từ Laravel quay lại đúng field;
  • nếu update ảnh, request dùng multipart + method spoofing;
  • redirect thành công trả về page props mới và thumbnail mới.
Luồng upload ảnh Product từ React useForm qua FormData đến Laravel và redirect Inertia

1. Schema chỉ lưu path, không lưu binary

Database chỉ cần giữ đường dẫn tương đối. File thực tế nằm ở public disk. Sau khi copy sample vào Laravel project, chạy:

2. Form data có cả file và intent xóa ảnh

image là state tạm ở browser. remove_image là intent rõ ràng để backend phân biệt:
Đây là cách tránh semantics mơ hồ của null.

3. Preview phải là local UI state

Khi user chọn file, không cần upload ngay chỉ để preview:
Điểm quan trọng là URL.revokeObjectURL(). Nếu người dùng chọn ảnh nhiều lần mà không cleanup, tab có thể giữ các blob URL cũ lâu hơn cần thiết.
Browser không thể khôi phục giá trị của <input type="file"> khi back/forward vì lý do bảo mật. Keyed useForm vẫn hữu ích cho các field text/error state, nhưng người dùng phải chọn lại file nếu rời trang trước khi submit.

4. File input cập nhật trực tiếp useForm

Không cần tự tạo FormData cho happy path. Inertia có thể chuyển data có file sang multipart request. Sample vẫn đặt forceFormData: true để intent rõ và để Edit luôn dùng cùng request shape.

5. Upload progress là form lifecycle state

Không cần tự subscribe XHR upload event cho use case này. Mental model:

6. Validation vẫn ở Laravel

Frontend accept= chỉ là UX hint. Nó không thay thế server validation. Nếu Laravel reject file:
React không cần tự parse response 422 để map error bag.

7. Create: POST multipart bình thường

8. Edit: dùng POST + method spoofing

Multipart với PUT/PATCH có thể gây giới hạn ở một số server/framework stack. Pattern an toàn là gửi POST nhưng thêm _method:
Laravel vẫn route request như PUT.

9. Replace ảnh mà không xóa ảnh cũ quá sớm

Backend sample làm theo thứ tự:
Nếu DB update fail sau khi đã store ảnh mới, sample xóa file mới trong catch để giảm orphan file. Không nên:

10. Delete Product cũng cleanup file

Ở production lớn hơn, bạn có thể chuyển cleanup sang job/event nếu storage chậm, nhưng sample giữ flow đồng bộ để dễ học.

11. Index chỉ nhận image_url, không biết storage path

Server tạo presentation prop:
Frontend nhận đúng thứ nó cần:
Điều này tốt hơn gửi image_path rồi để React tự đoán public URL/storage convention.

12. Thumbnail ở table

Thumbnail trong bảng là decoration vì tên sản phẩm đã nằm ngay cạnh, nên alt="" tránh screen reader đọc lặp.

13. Vì sao ví dụ ảnh giúp hiểu Inertia rõ hơn?

CRUD text-only có thể khiến Inertia trông giống một router + form helper. File upload cho thấy nhiều lớp runtime hoạt động cùng nhau:
Mà bạn vẫn không phải tự dựng:

Full source

Sample dùng local public disk để dễ copy và học. Production có thể dùng S3/object storage; khi đó vẫn giữ nguyên mental model Inertia, chỉ thay storage implementation và cách sinh image_url.

Tài liệu chính thức

Nội dung thực chiến trong bài được xây dựng dựa trên API và nguyên lý của Inertia.js v3 Documentation. Khi áp dụng vào dự án, hãy đối chiếu API cụ thể với tài liệu chính thức theo phiên bản bạn đang sử dụng.