reactjsnext.js

Using same components in a page cause bugs in Next.js


UPDATE: I created a codesandbox for my issue. You will see 3 components that you can upload files but whenever you choose a media, always the first component will update. https://codesandbox.io/p/devbox/vigorous-joana-rzqwcv

I use Next.js latest version. I have a component called "InputMedia". I use this component 3 times in a page with different props. My problem is: State update is always being done by the first component's information. Even though I click 3rd component always the first component's state changes. I tried to send type as a prop, didn't work. I tried to send important things as arguments didn't work again.

I alwas played with handleMediaChange function, InputMedia props and InputMedia component.

I will be glad if I can learn what am I missing.

Here is my Page:

"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
        }
      /> */
}

And InputMedia component:

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>
    </>
  );
}

Solution

  • I've checked your codesandbox and see the problems comes from your InputMedia.tsx component.

    If you want to use id attribute to every one of your components, then you have to make sure that every ID you have inside your app is unique.

    For example, in your InputMedia.tsx component:

    <input
       // To make sure your ID is unique, you can pass the type, for example
       id={type}
       type="file"
       className="hidden"
       onChange={handleMediaChange}
    />
    

    And if your every input already have unique ID, you also need to make sure that your label also point to the correct ID, for example, inside your InputMedia.tsx component:

    <label
       htmlFor={type}
    >
    .....
    </label>
    

    And inside your page.tsx just pass the type props to every of your InputMedia.tsx component:

    <InputMedia
        media={formItems.results[tabIndexStore.resultTabIndex].resultMedia}
        handleMediaChange={(e) => {
           console.log("result type input change");
           handleMediaChange(e, "result", tabIndexStore.resultTabIndex);
        }}
        // pass the type here for example
        type="result"
    />
    
    <InputMedia
        media={formItems.results[tabIndexStore.resultTabIndex].resultMedia}
        handleMediaChange={(e) => {
           console.log("question type input change");
           handleMediaChange(e, "result", tabIndexStore.resultTabIndex);
        }}
        // here's another type
        type="question"
    />
    

    The important point is that your ID attributes cannot be identical.

    Another example is to pass id props from your page.tsx like this:

    <InputMedia
        media={formItems.results[tabIndexStore.resultTabIndex].resultMedia}
        handleMediaChange={(e) => handleMediaChange(e, "result", tabIndexStore.resultTabIndex)}
        // will generate unique id over here
        id={`resultMediaInput-${tabIndexStore.resultTabIndex}`}
    />