You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js中react-sortablejs结合本地图片src不显示问题求助

问题:Next.js中使用react-sortablejs处理本地图片时src路径无法渲染

在Next.js项目里用react-sortablejs组件处理本地图片,排序后图片的src路径失效,控制台报错Image is missing required "src" property:。相关代码如下:

import { useState } from "react";
import axios from "axios";
import { useRouter } from "next/router";
import Image from "next/image";
import Spinner from "./Spinner";
import { ReactSortable } from "react-sortablejs";

export default function ProductForm({
  _id,
  title: existingTitle,
  description: existingDescription,
  price: existingPrice,
  images: existingImages,
}) {
  const [title, setTitle] = useState(existingTitle || "");
  const [description, setDescription] = useState(existingDescription || "");
  const [price, setPrice] = useState(existingPrice || "");
  const [images, setImages] = useState(existingImages || []);
  const [goToProducts, setGoToProducts] = useState("");
  const [isUploading, setIsUploading] = useState(false);
  const router = useRouter();
  async function saveProduct(ev) {
    ev.preventDefault();
    const data = { title, description, price, images };
    if (_id) {
      await axios.put("/api/products", { ...data, _id });
    } else {
      await axios.post("/api/products", data);
    }
    setGoToProducts(true);
  }
  if (goToProducts) {
    router.push("/products");
  }
  async function uploadImages(ev) {
    const files = ev.target?.files;
    if (files?.length > 0) {
      setIsUploading(true);
      const data = new FormData();
      for (const file of files) {
        data.append("file", file);
      }
      const res = await axios.post("/api/upload", data);
      for (let i = 0; i < res.data.path.length; i++) {
        console.log(res.data.path[i]);
      }
      setImages((OldImages) => {
        return [...OldImages, ...res.data.path];
      });
      setIsUploading(false);
    }
  }
  function updateImagesOrder(images) {
    setImages(images);
  }
  return (
    <form onSubmit={saveProduct}>
      <label>Nazwa Produkt</label>
      <input
        type="text"
        placeholder="Nazwa Produktu"
        value={title}
        onChange={(ev) => setTitle(ev.target.value)}
      />
      <label>Opis Produktu</label>
      <textarea
        placeholder="Opis"
        value={description}
        onChange={(ev) => setDescription(ev.target.value)}
      ></textarea>
      <label>Zdjęcia Produktu</label>
      <div className="mb-2 flex flex-wrap gap-1">
        <ReactSortable
          list={images}
          className="flex flex-wrap gap-1"
          setList={updateImagesOrder}
        >
          {images?.length &&
            images.map((path) => (
              <div key={path}>
                <Image
                  src={path}
                  alt="product_image"
                  width={0}
                  height={0}
                  sizes="6rem"
                  style={{ width: "100%", height: "100px" }}
                  className="rounded-lg"
                />
              </div>
            ))}
        </ReactSortable>
        {isUploading && (
          <div className="h-24 flex items-center">
            <Spinner />
          </div>
        )}
        <label className="w-24 h-24 cursor-pointer text-center flex items-center justify-center text-sm gap-1 text-gray-500 rounded-lg bg-gray-200">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            strokeWidth={1.5}
            stroke="currentColor"
            className="w-6 h-6"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"
            />
          </svg>
          <div>Wgraj</div>
          <input
            type="file"
            onChange={uploadImages}
            className="hidden"
            multiple
          ></input>
        </label>
      </div>
      <label>Cena Produktu</label>
      <input
        type="number"
        placeholder="Cena"
        value={price}
        onChange={(ev) => setPrice(ev.target.value)}
      />
      <button type="submit" className="btn-primary">
        Zapisz Produkt
      </button>
    </form>
  );
}

调试发现:当updateImagesOrder仅执行console.log(images)时,图片显示正常,控制台输出的数组元素是带额外属性的字符串:

(2) [String, String] 
0 :  String {'/2024-02-07T15-43-47.272Z-Fundamental.png', chosen: false, selected: false} 
1 :  String {'/2024-02-07T15-43-47.260Z-css-flexbox-poster.png', chosen: false, selected: false} 
length :  2

但改成setImages(images)后,图片无法显示,触发src缺失的错误。


解决方法

问题根源是react-sortablejs会给数组中的每个元素添加额外的属性(比如chosen、selected),原本的字符串会被包装成一个带这些属性的类字符串对象。当你把这个修改后的数组设置回state时,Image组件的src接收的不再是纯字符串,而是一个对象,导致Next.js无法识别路径。

解决步骤:

  1. 在更新state时,提取纯字符串路径
    修改updateImagesOrder函数,把每个元素转换回纯字符串:

    function updateImagesOrder(images) {
      // 提取每个元素的原始字符串值,去掉sortable添加的额外属性
      const pureImagePaths = images.map(img => String(img));
      setImages(pureImagePaths);
    }
    
  2. 优化key值(可选但推荐)
    因为排序后数组元素的位置变化,用path做key是安全的,但如果有重复路径的情况,可以结合索引或者添加唯一标识。不过如果你的图片路径都是唯一的,当前的key={path}没问题。

  3. 验证上传逻辑(补充)
    确保上传接口返回的res.data.path是纯字符串数组,而不是其他格式。当前代码里的setImages((OldImages) => [...OldImages, ...res.data.path])是正确的,因为上传的路径是纯字符串。

这样修改后,排序后的数组里就只有纯字符串路径,Image组件的src就能正常识别,图片可以显示同时排序功能也能正常工作。


内容的提问来源于stack exchange,提问作者revoxer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 07:43:24