0

I'm fetching async object with an array and a value. I want to take the array and add that other value to the same array at a random index.

{
   incorrect_answers: ['item1', 'item2', 'item3'],
   correct_answer: 'item4'
}

// I expect this: ['item1', 'item2', 'item4', 'item3'] the index for 'item4' is random

I get the array with useState fine, but don't know how to add the other value in the same useState line:

  const [index, setIndex] = useState(0);
  const [answers, setAnswers] = useState([]);

useEffect(()=>{
    (async () => {
      const data = await fetch ('https://opentdb.com/api.php?amount=10&type=multiple');
      const json = await data.json();      
      setAnswers(json.results[index].incorrect_answers);

//if I use .splice here, I get only the correct_answer in the array

      answers.splice(random,0, json.results[index].correct_answer);
      setAnswers(answers)
    })();
  }, []);

UPDATE:

the complete project is here: https://codepen.io/metsuge/pen/qBxyrMY The idea is that when clicking Next Question, the index would update to show the next object in the array. But the index value in NextQuestion() and in the if statement is off by 1, so questions and answers are mixed

Gustė
  • 119
  • 3
  • 12
  • splice mutates the original array and the return value is array containing the deleted items or an empty array ... – KcH Jun 05 '22 at 10:09
  • 1
    I had asked a [similar question](https://stackoverflow.com/questions/72348734/react-not-registering-state-change-when-setting-previous-state-with-slight-modif) recently. The (accepted) fantastic answer by @NickParsons will surely help you understand this concept better. – HerrAlvé Jun 05 '22 at 10:47

2 Answers2

1
  1. Your 2nd setAnswer may point to the old answers array as calling setAnswer can async.
  const [index, setIndex] = useState(0);
  const [answers, setAnswers] = useState([]);

  useEffect(()=>{
    (async () => {
      const data = await fetch ('');
      const json = await data.json();      
      setAnswers(json.results[index].incorrect_answers);

      // answers may equal to []
      answers.splice(random,0, json.results[index].correct_answer);
      setAnswers(answers)
    })();
  }, []);
  1. React will batch your two setAnswers calls and will only call the 2nd setAnswer. So your incorrect_answers will never set to the answers array. Therefore you are actually splicing the initial empty array.
  1. You are mutating the original data. so somehow if you manage to set the incorrect_answers before insert the correct_answer and still uses answers.splice, then the original data will be mutate. So after you mutate the original data set, you call the setAnswers(answers) with mutated data. When react compares the old value to the new value provided in setAnswers it sees both are equal. So React will ignore state update.

How you can fix the issue

  const [index, setIndex] = useState(0);
  const [answers, setAnswers] = useState([]);

  useEffect(()=>{
    (async () => {
      const data = await fetch ('');
      const json = await data.json(); 

      const data = json.results[index].incorrect_answers;
      data.splice(random,0, json.results[index].correct_answer);
      
      setAnswers(data);
    })();
  }, []);

References

  1. https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
  2. https://reactjs.org/docs/react-component.html#state

UPDATE

I have update your QuizeComponent

function QuizComponent() {
  const [APIData, setAPI] = useState({});
  const [question, setQuestion] = useState("");
  const [index, setIndex] = useState(0);
  const [answers, setAnswers] = useState([]);
  const [objectsLength, setObjectsLength] = useState(0);

  useEffect(() => {
      (async () => {
      const data = await fetch(
        "https://opentdb.com/api.php?amount=10&type=multiple"
      );
      const json = await data.json();
      setAPI(json.results);
      setObjectsLength(json.results.length);
    })();
  }, [])
  

  useEffect(() => {
     if (APIData[index]) {
       const random = Math.floor(Math.random() * 5);
       const data = APIData[index];
       const cpy = [...data.incorrect_answers];
       cpy.splice(random, 0, APIData[index].correct_answer);
       
       setAnswers(cpy);
       setQuestion(data.question);
     }
  }, [index, APIData])
  

  const nextQuestion = function () {
    if (index !== objectsLength.length - 1) {
      setIndex(index + 1);
      setQuestion(APIData[index].question);
      setAnswers(APIData[index].incorrect_answers);
    } else {
      console.log("This is the last question.");
    }
  };

  return (
    <div className="QuizComponent">
      <h1>QUIZ COMPONENT</h1>
      <div>
        <Question question={question} APIData={APIData} index={index} />
      </div>

      <div>
        <button onClick={() => nextQuestion(index)}>Next question</button>
      </div>

      <div id="main-answers-container">
        {answers.map(function (item, i) {
          return <Answer key={i} answer={item} index={index} />;
        })}
      </div>
    </div>
  );
}
Dilshan
  • 2,797
  • 1
  • 8
  • 26
  • Thank you! This works, just want to check why: is useEffect used twice to separate the purpose of handling aync data? And now I understand why state is immutable, to update the array, the original has to be copied and then you can change the copy, not the original. – Gustė Jun 06 '22 at 08:56
  • 1
    @Gustė one reason why hooks were introduced back in the day was to prevent the complexity with life cycle methods. If you did the same thing in class component, you had to do your work on `componentDidMount` and `componentDidUpdate`. Here with hooks, keep the similar things together. Don't mix up un related things. Since you are fetching all the questions and answers in one request, you can keep it in separate useEffect which will only run after initial mount. Then updating sate based on index is another work which need to re run when `index` or `APIData` changed. – Dilshan Jun 06 '22 at 11:22
0

In my console your code is working fine. I am thinking that the problem must be that you are doing operations on your state variable. You can try to do a deep copy of the array, then use splice(), and then setState(). Something like this:

const deepCopy = [...answers]
deepCopy.splice(random,0, json.results[index].correct_answer);
setAnswers(deepCopy)
Chizaram Igolo
  • 907
  • 8
  • 18
vladc
  • 150
  • 1
  • 9