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

Next.js复用InputMedia组件引发状态更新异常问题求助

问题:多个InputMedia组件上传文件时仅第一个组件状态更新

页面中有3个可上传文件的InputMedia组件,但无论选择哪个组件上传媒体,始终只有第一个组件的状态会更新。使用最新版Next.js,将InputMedia组件复用3次并传入不同props,状态更新始终仅对应第一个组件的信息,即使点击第三个组件,也是第一个组件的状态发生变化。尝试过传入type作为props、将关键信息作为参数传递,均无效。

Manage页面代码

"use client";
import Button from "@/components/Button/Button";
import Input from "@/components/Input/Input";
import InputMedia from "@/components/InputMedia/InputMedia";
import Select from "@/components/Select/Select";
import Tab from "@/components/Tab/Tab";
import { useState } from "react";

export default function Manage() {
  const [formItems, setFormItems] = useState({
    title: "",
    description: "",
    category: "Psikoloji Testleri",
    mainMedia: {},
    mainMediaUrl: "",
    questions: [
      {
        questionTitle: "selam",
        questionMedia: {},
        questionMediaUrl: "",
        options: [
          { answer: "", points: [{ resultTitle: "", point: 0, id: "item1" }] },
        ],
      },
    ],
    results: [
      {
        resultTitle: "",
        resultDescription: "",
        resultMedia: {},
        resultMediaUrl: "",
      },
    ],
  });

  const [tabIndexStore, setTabIndexStore] = useState({
    questionTabIndex: 0,
    questionOptionTabIndex: 0,
    resultTabIndex: 0,
  });

  const handleMainPropertiesChange = (e) => {
    const { name, value } = e.target;

    setFormItems((prev) => ({
      ...prev,
      [name]: value,
    }));
  };

  const handleMediaChange = (e, type, index = null) => {
    console.log(type, index, "function");
    const { files } = e.target;
    if (!files || files.length === 0) return;

    const file = files[0];

    setFormItems((prev) => {
      let updatedItems = { ...prev };

      switch (type) {
        case "main":
          updatedItems.mainMedia = file;
          break;

        case "question":
          if (index !== null && index < updatedItems.questions.length) {
            updatedItems.questions[index].questionMedia = file;
          }
          break;

        case "result":
          if (index !== null && index < updatedItems.results.length) {
            updatedItems.results[index].resultMedia = file;
          }
          break;

        default:
          break;
      }

      return updatedItems;
    });
  };

  const handleQuestionChange = (e, index) => {
    const { name, value } = e.target;
    const newQuestions = [...formItems.questions];
    newQuestions[index][name] = value;
    setFormItems((prev) => ({
      ...prev,
      questions: newQuestions,
    }));
  };

  const updateTabIndex = (index, item) => {
    setTabIndexStore((prev) => ({
      ...prev,
      [item]: index,
    }));
  };

  return (
    <form>
      <Input
        name="title"
        type="text"
        handleFunction={handleMainPropertiesChange}
        value={formItems.title}
      />
      <hr />
      <Input
        name="description"
        type="text"
        handleFunction={handleMainPropertiesChange}
        value={formItems.description}
      />
      <hr />
      <Select
        name="category"
        handleFunction={handleMainPropertiesChange}
        value={formItems.category}
      />
      <hr />
      <InputMedia
        media={formItems.mainMedia}
        handleMediaChange={(e) => handleMediaChange(e, "main")}
        /* handleMediaChange={handleMediaChange}
        type="main" */
      />
      <hr />
      <h1>Questions</h1>

      <Tab
        items={formItems.questions}
        handleFunction={updateTabIndex}
        currentIndex={tabIndexStore.questionTabIndex}
        itemName="questionTabIndex"
      />
      <Input
        name="questionTitle"
        type="text"
        handleFunction={handleQuestionChange}
        index={tabIndexStore.questionTabIndex}
        value={
          formItems.questions[tabIndexStore.questionTabIndex].questionTitle
        }
      />
      <InputMedia
        media={
          formItems.questions[tabIndexStore.questionTabIndex].questionMedia
        }
        handleMediaChange={(e) =>
          handleMediaChange(e, "question", tabIndexStore.questionTabIndex)
        }
        /*  handleMediaChange={handleMediaChange}
        type="question"
        index={tabIndexStore.questionTabIndex} */
      />

      <h1>Question Options</h1>
      <Tab
        items={formItems.questions[tabIndexStore.questionTabIndex].options}
        handleFunction={updateTabIndex}
        currentIndex={tabIndexStore.questionOptionTabIndex}
        itemName="questionOptionTabIndex"
      />
      <Input name="option" type="text" />
      <Input name="point" type="number" />
      <Button type="button" isButtonSecondary={true} name="Add new option" />
      <br />
      <br />
      <Button type="button" isButtonSecondary={true} name="Add new question" />
      <hr />
      <h1>Results</h1>
      <Tab
        items={formItems.results}
        handleFunction={updateTabIndex}
        currentIndex={tabIndexStore.resultTabIndex}
        itemName="resultTabIndex"
      />
      <Input name="result title" type="text" />
      <Input name="result description" type="text" />
      <InputMedia
        media={formItems.results[tabIndexStore.resultTabIndex].resultMedia}
        handleMediaChange={(e) =>
          handleMediaChange(e, "result", tabIndexStore.resultTabIndex)
        }
        /*  type="result"
        index={tabIndexStore.resultTabIndex} */
      />
      <Button type="button" isButtonSecondary={true} name="Add new result" />
      <br />
      <br />
      <Button type="button" isButtonSecondary={false} name="Submit" />
    </form>
  );
}

{
  /* <Input
        name="questionMedia"
        type="file"
        handleFunction={handleQuestionChange}
        index={tabIndexStore.questionTabIndex}
        value={
          formItems.questions[tabIndexStore.questionTabIndex].questionMedia
        }
      /> */
}

InputMedia组件代码

export default function InputMedia({
  media,
  handleMediaChange,
  /*  type,
  index = null, */
}) {
  return (
    <>
      Media
      <label
        htmlFor="dropzone-file"
        className="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-bray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600"
        onClick={(e) => {
          e.preventDefault();
          console.log("uffufu");
        }}
      >
        <div className="flex flex-col items-center justify-center pt-5 pb-6">
          <svg
            className="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400"
            aria-hidden="true"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 20 16"
          >
            <path
              stroke="currentColor"
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
            />
          </svg>
          <p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
            <span className="font-semibold">Click to upload</span> or drag and
            drop
          </p>
          {media instanceof File && (
            <p className="text-xs text-gray-500 dark:text-gray-400">
              Uploaded media name: {media.name}
            </p>
          )}
        </div>
        <input
          id="dropzone-file"
          type="file"
          className="hidden"
          onChange={handleMediaChange}
        />
      </label>
    </>
  );
}
解决方案

问题根源在于所有InputMedia组件中的file input使用了相同的id dropzone-file。在HTML中,id必须是唯一的,当多个元素共享同一个id时,点击label或触发input时,浏览器只会找到第一个匹配该id的元素,导致始终只有第一个InputMedia的状态被更新。

修复步骤:

  1. 给每个InputMedia组件传入唯一的id属性
  2. 在InputMedia组件中使用传入的id来关联label和input

修改Manage页面中InputMedia的调用

// 第一个InputMedia
<InputMedia
  media={formItems.mainMedia}
  handleMediaChange={(e) => handleMediaChange(e, "main")}
  id="main-media-dropzone"
/>

// 第二个InputMedia(Question部分)
<InputMedia
  media={formItems.questions[tabIndexStore.questionTabIndex].questionMedia}
  handleMediaChange={(e) => handleMediaChange(e, "question", tabIndexStore.questionTabIndex)}
  id={`question-media-dropzone-${tabIndexStore.questionTabIndex}`}
/>

// 第三个InputMedia(Result部分)
<InputMedia
  media={formItems.results[tabIndexStore.resultTabIndex].resultMedia}
  handleMediaChange={(e) => handleMediaChange(e, "result", tabIndexStore.resultTabIndex)}
  id={`result-media-dropzone-${tabIndexStore.resultTabIndex}`}
/>

修改InputMedia组件

export default function InputMedia({
  media,
  handleMediaChange,
  id, // 接收id属性
}) {
  return (
    <>
      Media
      <label
        htmlFor={id} // 使用传入的id关联label
        className="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-bray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600"
        onClick={(e) => {
          e.preventDefault();
          console.log("uffufu");
        }}
      >
        <div className="flex flex-col items-center justify-center pt-5 pb-6">
          <svg
            className="w-8 h-8 mb-4 text-gray-500 dark:text-gray-400"
            aria-hidden="true"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 20 16"
          >
            <path
              stroke="currentColor"
              stroke-linecap="round"
              stroke-linejoin="round"
              stroke-width="2"
              d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
            />
          </svg>
          <p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
            <span className="font-semibold">Click to upload</span> or drag and
            drop
          </p>
          {media instanceof File && (
            <p className="text-xs text-gray-500 dark:text-gray-400">
              Uploaded media name: {media.name}
            </p>
          )}
        </div>
        <input
          id={id} // 使用传入的id作为input的id
          type="file"
          className="hidden"
          onChange={handleMediaChange}
        />
      </label>
    </>
  );
}

这样每个file input都有了唯一的id,label和input的关联关系正确,点击不同的InputMedia组件时,会触发对应的input元素,状态更新也会对应到正确的组件上。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 00:57:34