reactjsjsxarray-splice

React Splice() isn't Preserving the state of array


So I am Trying to implement Google Forms Clone and I want to add new question div at a specific index. But my div array's state gets lost.

//array of Divs
const [questionDivs, setQuestionDivs] = useState([]);

//Function to add new div to the questionDivs at a specific index
 const addQuestionDiv = () => {
    const newKey = `question_${questionDivs.length + 1}`;
    const newQuestionDivs = [...questionDivs];
    

    newQuestionDivs.splice(selectedDivIndex + 1, 0, (
      <div
        key={newKey}
        className="my-[1%] ml-[28%] w-[44%] min-h-[30%]"
        onClick={handleQuestionClick}
      >
        {<Questions key={newKey}/>}
      </div>
    ));
  
   setQuestionDivs(newQuestionDivs);
  };


//Rendering the divs
 {questionDivs.map((questionDiv, index) => (
          <div key={`question_${index}`}  onClick={()=> SetCurrentDivHandler(index)}>{questionDiv}</div>
        ))}

[Here are 2 question divs and when i press the + icon which i Highlighted in the image, I want a new div to be added right below the current selected div, I have already implemented a function where you get the index of the current div. ] (https://i.sstatic.net/jskYD.png) [When I press the + button a new div is getting added but the div which is below it is loosing its state, It had Option A and Option B but now it is empty after Insertion of new Div. But the div above that didn't loose its state. To whichever index I try to add a new div, the indexes which comes after that looses their states. I even tried looping but same results] (https://i.sstatic.net/a8OAT.png)


Solution

  • Problem

    React uses keys as part of the decision to update the DOM. A comparison is made between the old DOM tree and the new DOM tree. For elements with the same key, React will decide it can keep the DOM trees the same.

    {
      questionDivs.map((questionDiv, index) => (
        <div
          key={`question_${index}`}
          onClick={() => SetCurrentDivHandler(index)}
        >
          {questionDiv}
        </div>
      ))
    }
    

    As you map over questionDivs you return a div with its key essentially set to its index position in the array (we can ignore the question_ prefix because that is constant). When you insert something, all subsequent items will have their keys changed, resulting in React rebuilding those trees and losing state. Here's a demo of the problem, note the div using the index key wrapping the Question component – this demo is also reused in the solution so a comparison can be easily made.

    const { createRoot } = ReactDOM;
    const { StrictMode, useEffect, useState } = React;
    
    function Question({ addQuestion, updateQuestion }) {
      const [title, setTitle] = useState("");
      const [state, setState] = useState("");
    
      const handleTitleChange = (event) => {
        setTitle(event.target.value);
      };
      
      const handleStateChange = (event) => {
        setState(event.target.value);
      };
    
      return (
        <div>
          <input
            type="text"
            placeholder="Question"
            value={title}
            onChange={handleTitleChange}
          />
          <br/>
          <textarea
            value={state}
            placeholder="Some other state"
            onChange={handleStateChange}
          />
          <button aria-label="Add after" onClick={addQuestion}>✚</button>
        </div>
      );
    }
    
    function App() {
      const [questions, setQuestions] = useState([crypto.randomUUID()]);
      
      const addQuestionAfter = (index) => {
        const updatedQuestions = [...questions];
        updatedQuestions.splice(index + 1, 0, crypto.randomUUID());
        setQuestions(updatedQuestions);
      };
      
      return (
        <div>
          {
            questions.map((questionKey, index) => (
              <div key={index}>
                <Question
                  key={questionKey}
                  addQuestion={() => addQuestionAfter(index)}
                />
              </div>
            ))
          }
        </div>
      );
    }
    
    const root = createRoot(document.getElementById("root"));
    root.render(<StrictMode><App /></StrictMode>);
    body {
      font-family: san-serif;
    }
    
    input, textarea {
      width: 50vw;
    }
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

    Solution

    You need more stable keys. IDs of some sort are a good choice. Not critical to the solution but generally important: you should also consider thinking about views as a representation of data – this means instead of storing parts of the view as JSX inside an array, just storing the data that represents it and mapping it across to the JSX representation. Below is a demo, note the following:

    1. We're not storing JSX inside an array. We're only storing data (UUIDs) inside the array, then mapping the data to JSX (Questions component).
    2. Each key is unique and is stable (it does not depend on the order of the array).

    The demo is also naive in the sense that the state is an array of IDs. A more complex application might store a list/map of objects representing the state of each question instead which would be kept in sync with a server. However, given the complexity, I won't show that in the demo.

    const { createRoot } = ReactDOM;
    const { StrictMode, useEffect, useState } = React;
    
    function Question({ addQuestion, updateQuestion }) {
      const [title, setTitle] = useState("");
      const [state, setState] = useState("");
    
      const handleTitleChange = (event) => {
        setTitle(event.target.value);
      };
      
      const handleStateChange = (event) => {
        setState(event.target.value);
      };
    
      return (
        <div>
          <input
            type="text"
            placeholder="Question"
            value={title}
            onChange={handleTitleChange}
          />
          <br/>
          <textarea
            value={state}
            placeholder="Some other state"
            onChange={handleStateChange}
          />
          <button aria-label="Add after" onClick={addQuestion}>✚</button>
        </div>
      );
    }
    
    function App() {
      const [questions, setQuestions] = useState([crypto.randomUUID()]);
      
      const addQuestionAfter = (index) => {
        const updatedQuestions = [...questions];
        updatedQuestions.splice(index + 1, 0, crypto.randomUUID());
        setQuestions(updatedQuestions);
      };
      
      return (
        <div>
          {
            questions.map((questionKey, index) => (
              <Question
                key={questionKey}
                addQuestion={() => addQuestionAfter(index)}
              />
            ))
          }
        </div>
      );
    }
    
    const root = createRoot(document.getElementById("root"));
    root.render(<StrictMode><App /></StrictMode>);
    body {
      font-family: san-serif;
    }
    
    input, textarea {
      width: 50vw;
    }
    <div id="root"></div>
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>