3

I have a list of div's that contain question fields. For every button click i add a new line of question fields and memorize in state the whole list and how many lines there are. I tried adding a delete button but when my delete functions it seems that the value from the state variable is remembered from when the line was made. How do i solve this so i can acces the full list in the HandleDelete function?

const OefeningAanvragenDagboek = () => {
const { t, i18n } = useTranslation()
const [questionCount, setQuestionCount] = useState(1)
const [questionList, setQuestionList] = useState([])

const createNewLine = () =>{
    var newLine=
        <div>
            <Field type="text" name={"vraag" + questionCount}/>
            <Field component="select" name={"antwoordMogelijkheid"+questionCount}>
                <option value="">...</option>
                <option value="open">{t('oefeningAanvragenDagboek.open')}</option>
                <option value="schaal">{t('oefeningAanvragenDagboek.scale')}</option>
            </Field>
            <Field type="text" name={"type"+questionCount}/>
            <button onClick={() => HandleDelete(questionCount-1)}>{t('assignmentCard.delete')}</button>
        </div>

      setQuestionList(questionList => [...questionList, newLine])
      setQuestionCount(questionCount+1)
  }

  const HandleDelete = (index)=> {
      console.log(questionList)
      // setQuestionList(questionList.splice(index, 1))

  }

  return (
      <div>
          <button onClick={createNewLine}>{t('oefeningAanvragenDagboek.addQuestion')}</button>
          {questionList}
      </div>
  )
}
Dipen Shah
  • 25,562
  • 1
  • 32
  • 58
PieterjanV
  • 31
  • 1
  • [`splice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) mutates the original array and returns the deleted items. You should never mutate state. Your mutation could be overwritten, and I doubt you want to update it to only include the deleted values. – Brian Thompson Mar 12 '20 at 17:07
  • I'm having the same issue, was there ever a solution for this? – SNYDERHAUS Oct 05 '20 at 20:53
  • the answer below is a solution to this @BryceSnyder. can you attach a minimal example of your issue via http://codesandbox.io or similar? – 95faf8e76605e973 Oct 06 '20 at 00:23
  • @95faf8e76605e973 threw this together.. https://codesandbox.io/s/charming-architecture-9kp71?file=/src/App.js if you click on either of the close after adding toasts, it just explodes the list. attempted to do the "answer" from below, but, it just breaks further. – SNYDERHAUS Oct 06 '20 at 00:58
  • just use `toastId` instead of `id` https://codesandbox.io/s/agitated-lewin-5qmsi?file=/src/App.js since that is the name of the object key when you invoke `addToast` and add a new `list` array element – 95faf8e76605e973 Oct 06 '20 at 01:14
  • 1
    @95faf8e76605e973 I forgot I was refactoring, added ID back. It works if you click the X but if you click the button it does not and is wonky. That's where the issue is specifically. If you click 5x and then click on the first one you created, the whole list is deleted. if you click 5x and then click on the instance that was just created, you'll get the desired functionality... – SNYDERHAUS Oct 06 '20 at 01:17
  • I added an answer that solves your issue @BryceSnyder – 95faf8e76605e973 Oct 06 '20 at 02:07
  • @95faf8e76605e973 Ya bud! thank you, appreciate it! I knew I wasn't far from the OP.. but, something wasn't working out for me.. – SNYDERHAUS Oct 06 '20 at 02:56

3 Answers3

1

Use functional setState as HandleDelete has closure on questionList

setQuestionList(questionList => questionList.splice(index, 1))

Both state and props received by the updater function are guaranteed to be up-to-date.

Dennis Vash
  • 50,196
  • 9
  • 100
  • 118
  • [`splice`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) mutates the original array and returns the deleted items. This is against the rules of react and also probably doesn't do what they want. – Brian Thompson Mar 12 '20 at 17:05
  • yea, forgot about it, need to fix the answer and return a copy – Dennis Vash Mar 12 '20 at 17:13
1

Primarily addressing the issue on the OP's comments section, at the time of this writing which was given a bounty in addition to the question.

The SandBox with the issue: https://codesandbox.io/s/charming-architecture-9kp71?file=/src/App.js

Basically, the solution to this issue is to perform all operations on the parameter of the callback function. In the case of the sandbox issue I linked above, if you look at removeToast on the code below, the operations are being done on the list array.

Code with the issue:

export default function App() {
  const [list, setList] = useState([]);

  const removeToast = (id) => {
    const newList = list.filter(({ toastId }) => toastId !== id);
    setList([...newList]);
  };

  const addToast = () => {
    const toastId = Math.random().toString(36).substr(2, 9);
    const newList = [
      ...list,
      {
        toastId,
        content: (
          <>
            <button onClick={() => removeToast(toastId)}>Hi there</button>
            Hello {toastId}
          </>
        )
      }
    ];
    setList([...newList]);
  };

  return (
    <>
      <button onClick={addToast}>Show Toast</button>
      <Toaster removeToast={removeToast} toastList={list} />
    </>
  );
}

However since removeToast has a closure on list, we need to do the filtering on the previous state which is, again, accessible via the first parameter of the callback of setState

The fix:

const removeToast = (id) => {
  setList((prev) => {
    return prev.filter(({ toastId }) => toastId !== id);
  });
};

The Solution: https://codesandbox.io/s/suspicious-silence-r1n41?file=/src/App.js

95faf8e76605e973
  • 13,643
  • 3
  • 24
  • 51
  • 1
    Thanks for the timely help, appreciated! I was about 90% of the way there.. unfortunately, I forgot to add the array spread on the return :| – SNYDERHAUS Oct 08 '20 at 16:31
  • 1
    There is no point spreading filtered results since `filter` already creates a new array. ie. `setList((prev) => prev.filter(({ toastId }) => toastId !== id));` – SILENT Oct 08 '20 at 21:26
1

You can pass event handler from container to child and then invoke event handler from client.

For example, let's say I have an app displaying list of items and each item have a delete button to remove them from the list. In this case, parent component will supply list of items and event handler to child component and then, child will be responsible for rendering and calling event handler.

Take a look at this codesandbox from which I am pasting following code:

import React, { useState } from "react";
import "./styles.css";

export function List(props) {
  return (
    <div>
      {props.items.map((i, idx) => (
        <div class="item" key={idx}>
          {i} <span onClick={() => props.onDelete(idx)}>X</span>
        </div>
      ))}
    </div>
  );
}

export default function App() {
  const [items, setItems] = useState([
    "Item 1",
    "Item 2",
    "Item 3",
    "Item 4",
    "Item 5",
    "Item 6"
  ]);

  const deleteItem = (index) => {
    if (index >= 0 && index < items.length) {
      const newItems = items.slice();
      newItems.splice(index, 1);
      setItems(newItems);
    }
  };

  return (
    <div className="App">
      <List items={items} onDelete={deleteItem}></List>
    </div>
  );
}

Dipen Shah
  • 25,562
  • 1
  • 32
  • 58