Component lose its state when re-renders

I have a sample where I have a list of answers and a paragraph that has gaps. I can drag the answers from the list to the paragraph gaps. After the answer fills the gap, the answer will be removed from the list. 3 answers and 3 gaps, the answer list should be empty when i drag all of them to the gaps.

enter image description here

But whenever I filter the listData, the component re-renders and the listData gets reset. The list always remained 2 items no matter how hard I tried. What was wrong here?

My code as below, I also attached the code sandbox link, please have a look

App.js

import GapDropper from "./gapDropper";
import "./styles.css";

const config = {
  id: "4",
  sort: 3,
  type: "gapDropper",
  options: [
    {
      id: "from:drop_gap_1",
      value: "hello"
    },
    {
      id: "from:drop_gap_2",
      value: "person"
    },
    {
      id: "from:drop_gap_3",
      value: "universe"
    }
  ],
  content: `<p>This is a paragraph. It is editable. Try to change this text.  <input id="drop_gap_1" type="text"/> . The girl is beautiful  <input id="drop_gap_2" type="text"/> I can resist her charm. Girl, tell me how  <input id="drop_gap_3" type="text"/></p>`
};

export default function App() {
  return (
    <div className="App">
      <GapDropper data={config} />
    </div>
  );
}

gapDropper.js

import { useEffect, useState } from "react";
import * as _ from "lodash";

import styles from "./gapDropper.module.css";

const DATA_KEY = "answerItem";

function HtmlViewer({ rawHtml }) {
  return (
    <div>
      <div dangerouslySetInnerHTML={{ __html: rawHtml }} />
    </div>
  );
}

function AnwserList({ data }) {
  function onDragStart(event, data) {
    event.dataTransfer.setData(DATA_KEY, JSON.stringify(data));
  }

  return (
    <div className={styles.dragOptionsWrapper}>
      {data.map((item) => {
        return (
          <div
            key={item.id}
            className={styles.dragOption}
            draggable
            onDragStart={(event) => onDragStart(event, item)}
          >
            {item.value}
          </div>
        );
      })}
    </div>
  );
}

function Content({ data, onAfterGapFilled }) {
  const onDragOver = (event) => {
    event.preventDefault();
  };

  const onDrop = (event) => {
    const draggedData = event.dataTransfer.getData(DATA_KEY);
    const gapElement = document.getElementById(event.target.id);
    const objData = JSON.parse(draggedData);

    gapElement.value = objData.value;
    onAfterGapFilled(objData.id);
  };

  function attachOnChangeEventToGapElements() {
    document.querySelectorAll("[id*='drop_gap']").forEach((element) => {
      element.ondragover = onDragOver;
      element.ondrop = onDrop;
    });
  }

  useEffect(() => {
    attachOnChangeEventToGapElements();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div>
      <HtmlViewer rawHtml={data} />
    </div>
  );
}

const GapDropper = ({ data }) => {
  const [gaps, setGaps] = useState(() => {
    return data.options;
  });

  function onAfterGapFilled(id) {
    let clonedGaps = _.cloneDeep(gaps);

    clonedGaps = clonedGaps.filter((g) => g.id !== id);

    setGaps(clonedGaps);
  }

  return (
    <div>
      <div>
        <AnwserList data={gaps} />
        <Content
          data={data.content}
          onAfterGapFilled={(e) => onAfterGapFilled(e)}
        />
      </div>
    </div>
  );
};

export default GapDropper;

Code sandbox


the problem is that you are not keeping on track which ids you already selected, so thats why the first time it goes right, and then the second one, the values just replace the last id.

Without changing a lot of your code, we can accomplish by tracking the ids inside a ref.

const GapDropper = ({ data }) => {
  const [gaps, setGaps] = useState(() => {
    return data.options;
  });

  const ids = useRef([])

  function onAfterGapFilled(id) {
    let clonedGaps = _.cloneDeep(gaps);
    ids.current = [...ids.current, id]
    clonedGaps = clonedGaps.filter((g) => !ids.current.includes(g.id));
    setGaps(clonedGaps);
  }

  return (
    <div>
      <div>
        <AnwserList data={gaps} />
        <Content
          data={data.content}
          onAfterGapFilled={(e) => onAfterGapFilled(e)}
        />
      </div>
    </div>
  );
};

Maybe there is a better solution but this one does the job