Home > database >  setting state in react with nested .map functions possible?
setting state in react with nested .map functions possible?

Time:08-28

I am developing a simple multiple choice question and answer app using react component functions.

Each question has 2 or 4 possible answers where I have the data configured in nested objects / arrays an example is like below:

{
question: 'what country is....',
id: nanoid(),
answers: 
[{
  answer: 'Egypt',
  id: nanoid(),
  isCorrect: true,
  isChecked: false
},
{
  answer: 'Greece',
  id: nanoid(),
  isCorrect: false,
  isChecked: false
},
{......}

my problem is when I want to flip the 'isChecked' attribute (when someone clicks on the corresponding button) I need to iterate through the object and then iterate through the 'answers' array, however I am unable to set state correctly, I tried nested map methods, I also am trying nested for loops, but I just can't get the desired results no matter what I try.

What is a simple method when having nested objects and arrays to setState correctly and reliably?

here is my code below

App.js

import React from 'react'
import Questions from './Questions'
import { nanoid } from 'nanoid'
import { decode } from 'he'

export default function App() {
  const [tempState, setTempState] = React.useState(true)
  const [data, setData] = React.useState([])


  React.useEffect(() => {
    fetch("https://opentdb.com/api.php?amount=5&category=9&difficulty=medium")
      .then(res => res.json())
      .then(info => stateFormat(info.results))
  }, [])


  function stateFormat(rawData) {
    // console.log(rawData)
    let correctAnswers = [];
    for (let i = 0; i < rawData.length; i  ) {
      correctAnswers.push({
        question: rawData[i].question,
        id: nanoid(),
        answers: [{
          answer: rawData[i].correct_answer,
          id: nanoid(),
          isCorrect: true,
          isChecked: false
        }]
      })
      if (rawData[i].incorrect_answers.length > 2) {
        for (let j = 0; j < 3; j  ) {
          correctAnswers[i].answers.push({
            answer: rawData[i].incorrect_answers[j],
            id: nanoid(),
            isCorrect: false,
            isChecked: false
          })
        }
      } else {
        correctAnswers[i].answers.push({
          answer: rawData[i].incorrect_answers[0],
          id: nanoid(),
          isCorrect: false,
          isChecked: false
        })
      }
    }
    setData(correctAnswers)
    console.log(data)

  }


  function handleChange(id) {
    setData(prev => {
      for(let i = 0; i < prev.length; i  ) {
        for(let j = 0; j < prev[i].answers.length; j  ) {
          // console.log(prev[i].answers[j])

          if(prev[i].answers[j].id === id) {
              console.log({...prev[i].answers[j], isChecked: !prev[i].answers[j].isChecked})
              return {...prev[i].answers[j], isChecked: !prev[i].answers[j].isChecked}
          } else {
            // return {...prev[i].answers[j]}
          }
        }
      }
    })
      
      
    
  }


  // function handleChange(id) {
  //   setData(prev => prev.map(item => {
  //    return (item.answers.map(button => {
  //       return button.id === id ?
  //       { ...button, isChecked: !button.isChecked } :
  //       button
  //     }))
      
  //   }))
  // }


  const questionElements = data.map(item => (
    <Questions
      key={item.id}
      question={item.question}
      answers={item.answers}
      handleChange={handleChange}
    />
  ))




  return (
    <main>
      <img className="blob--top"
        src={require('./blobs.png')}
      />
      <img className="blob--bottom"
        src={require('./blobs1.png')}
      />
      {tempState ?

        <div className="quiz--container">
          <div>
            {questionElements}
            <button className="game--check button">Check answers</button>
          </div>

        </div> :
        <>
          <div className="title--container">
            <div className="title--init">
              <h2 className="title--header">Quizzical</h2>
              <h4 className="title--subheader">A battle of the brains</h4>
              <button className="game--start button"
                onClick={() => setTempState(!tempState)}
              >
                Start quiz</button>
            </div>
          </div>
        </>
      }
    </main>
  );
}

Questions.js

import React from 'react'
import { decode } from 'he'

export default function Questions(props) {
 

    // console.log(props)

    const answerButton = props.answers.map(item => (
        <button
            isCorrect={item.isCorrect}
            isChecked={item.isChecked}
            id={item.id}
            style={{backgroundColor: item.isChecked ? "#D6DBF5" : ""}}
            onClick={() => props.handleChange(item.id)}>{decode(item.answer)}
        </button>
    ))

    return (
        <div className="question--container">
            <h4>{decode(props.question)}</h4>
            <div className="question--items">
                {answerButton}
            </div>
            <img src={require('./Line13.png')} />
        </div>

    )

}

the issue is with my handleChange(id) function, what can be done to return the appropriate object?

CodePudding user response:

Your .map() approach is very close, you just need to wrap your inner .map() in an object. The object should contain all the item properties, which you can create by spreading the current item with ...item, and then overwrite the answer property to be your mapped answers array:

function handleChange(id) {
  setData(prev => prev.map(item => {
    return {
      ...item,
      answers: item.answers.map(button => {
        return button.id === id
          ? { ...button, isChecked: !button.isChecked} 
          : button
      })
    };
  }));
}

With arrow functions, you can omit the function body {} and hence the return if all you're doing is returning from within the body. So the above can be rewritten as:

function handleChange(id) {
  setData(prev => prev.map(item => ({
    ...item,
    answers: item.answers.map(button => button.id === id
      ? { ...button, isChecked: !button.isChecked} 
      : button
    )
  })));
}

Note that if you want to make your code more efficient, you can also pass the question id when you call handleChange(), which will allow you to add an additional condition in your outer .map() so that you only perform your inner .map() when you're iterated the question being answered, for all the other questions you can then skip the inner .map() iteration on the answers array as you know these haven't changed.

  • Related