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

Shadcn React表单图片上传异常:上传第二张时数组被覆盖

问题:Shadcn React表单图片上传时数组被覆盖而非追加

基于Shadcn构建的React表单中,通过自定义ImageUpload组件(集成next-cloudinary上传组件)实现图片预览后上传至数据库的功能。单张图片上传一切正常,但上传第二张图片时,表单的images数组会被覆盖,而非追加新的图片对象。

相关代码

FormField代码

<FormField
  control={form.control}
  name="images"
  render={({ field }) => {
    return (
      <FormItem>
        <FormLabel>Images</FormLabel>
        <FormControl>
          <ImageUpload
            value={field.value.map((image) => image.url)}
            disabled={loading}
            onChange={(url) => field.onChange([...field.value, { url }])} // 新增图片时调用
            onRemove={(url) => field.onChange([...field.value.filter((current) => current.url !== url)])} // 删除图片时调用
          />
        </FormControl>
        <FormMessage />
      </FormItem>
    )
  }}
/>

ImageUpload组件代码(基于next-cloudinary)

'use client'

import { useEffect, useState } from 'react'
import { Button } from './button'
import { ImagePlus, Trash } from 'lucide-react'
import Image from 'next/image'

import { CldUploadWidget, CloudinaryUploadWidgetResults } from 'next-cloudinary'

interface ImageUploadProps {
  disabled?: boolean
  onChange: (value: string) => void
  onRemove: (value: string) => void
  value: string[]
}

function ImageUpload({ disabled, onChange, onRemove, value }: ImageUploadProps) {
  const [isMounted, setIsMounted] = useState(false)

  useEffect(() => {
     setIsMounted(true)
   }, [])

  console.log('value:', value)

  const onUpload = (result: CloudinaryUploadWidgetResults) => {
    const info = result.info as object

    console.log('start')
    console.log(value)

    if ('secure_url' in info) {
      const url = info.secure_url as string
      console.log('Uploaded URL:', url)
      onChange(url)
    }
  }

  if (!isMounted) return null

  return (
    <div>
      <div className="mb-4 flex items-center gap-4">
        {value.map((url) => (
          <div key={url} className="relative h-[200px] w-[200px] overflow-hidden rounded-md">
            <div className="absolute right-2 top-2 z-10">
              <Button type="button" onClick={() => onRemove(url)} variant="destructive" size="icon">
                <Trash className="h-4 w-4" />
              </Button>
            </div>
            <Image fill className="object-cover" alt="image" src={url} />
          </div>
        ))}
      </div>
      <CldUploadWidget onSuccess={onUpload} uploadPreset="erx1yvtk">
        {({ open }) => {
          const onClick = () => {
            open()
          }
          return (
            <Button type="button" disabled={disabled} variant="secondary" onClick={onClick}>
              <ImagePlus className="mr-2 h-4 w-4" />
              Upload an Image
            </Button>
          )
        }}
      </CldUploadWidget>
    </div>
  )
}

export default ImageUpload

尝试过的代码及错误

尝试用函数式更新修改状态,虽能正常预览两张图片,但出现错误提示:期望数组而非函数

const handleImageChange = (url: string) => {
  // 更新表单状态,添加新图片
  field.onChange((prev: Image[]) => [...(prev || []), { url }])
}

// 处理图片删除
const handleImageRemove = (url: string) => {
  // 更新表单状态,过滤掉要删除的图片
  field.onChange((prev: Image[]) => (prev || []).filter((image) => image.url !== url) as Image[])
}

问题原因

  1. 最初写法存在闭包陷阱:组件渲染时捕获的field.value是初始值,每次上传新图片都会基于这个初始值创建数组,导致之前的图片被覆盖。
  2. 函数式更新写法报错,是因为React Hook Form的field.onChange默认只接受具体值,不支持传入更新函数(setValue方法才支持函数式更新)。

解决方案

改用form.setValue实现函数式更新,确保每次基于最新表单状态修改数组:

<FormField
  control={form.control}
  name="images"
  render={({ field }) => {
    // 处理图片新增
    const handleImageChange = (url: string) => {
      form.setValue('images', (prev = []) => [...prev, { url }])
    }

    // 处理图片删除
    const handleImageRemove = (url: string) => {
      form.setValue('images', (prev = []) => prev.filter(image => image.url !== url))
    }

    return (
      <FormItem>
        <FormLabel>Images</FormLabel>
        <FormControl>
          <ImageUpload
            value={field.value.map((image) => image.url)}
            disabled={loading}
            onChange={handleImageChange}
            onRemove={handleImageRemove}
          />
        </FormControl>
        <FormMessage />
      </FormItem>
    )
  }}
/>

补充说明

  • form.setValue支持传入更新函数,参数为当前最新的字段值,可避免闭包导致的旧值问题。
  • 给prev设置默认值[],确保字段初始值为undefined时也能正常工作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 01:54:52