How to get answers from multiple components in ReactJS? - javascript

I'm working on a survey project, where I'm having a react component 'Questions', a child component 'QuestionOptions' with multiple children 'Radio', 'TextBox', 'Checkbox'.
Some questions in the database have options (multiple choice).
I want to find a way to gather all the answers answered by the user and send them to the database.
This is the Questions component:
const Questions = () => {
const [questions, setQuestions] = useState([])
var game_id = localStorage.getItem('game_id')
useEffect(() => {
axios
.post("http://127.0.0.1:8000/api/v1/questions/get_questions", {
game_id: game_id
})
.then((response) => {
const s = response.data.questions;
setQuestions(s);
});
}, []);
return (
<div>
<ul>
{questions.map((question) => (
<div key={question.id}>
<li key={question.id}>{question.question} ({question.points} points)</li>
<QuestionOptions question_id={question.id} question_type={question.question_type_id}/>
</div>
))}
</ul>
<Button text={"Submit"} id={"submit_btn"}/>
</div>
);
};
This is the QuestionOptions component:
const QuestionOptions = (props) => {
const [options, setOptions] = useState([]);
const question_type = props.question_type;
useEffect(() => {
axios
.post("http://127.0.0.1:8000/api/v1/question_options/get_options", {
question_id: props.question_id,
})
.then((response) => {
setOptions(response.data["options"]);
});
}, [props.question_id]);
if(question_type === 0){
return(
<TextBox />
)
}else if(question_type === 1){
return(
<Radio options={options}/>
)
}else if(question_type === 2){
return(
<Checkbox options={options}/>
)
}
};
The TextBox, Radio and Checkbox components looks as follows:
import React from "react";
const TextBox = () => {
return <input type={"text"} />;
};
export default TextBox;
So I want to get all the answers answered by the user, and disable the submit button if not all questions are answered.
The Questions component looks as follows:
What is your name?
.(input goes here)
What is your favorite color?
. option1
. option2
. option3
What is the best way to make that happen?

Here is example with two types of questions: ones with a save button option, others which update answers directly. Check the output of console.log after you click save button or type answer in the third question.
let questions = [
{ q: 'How old are you?', id: 0 },
{ q: 'How many pets do you have?', id: 1 },
{ q: 'Where were you born?', id: 2, type: 'NoSaveBtn' },
];
let Question = (props) => {
let [value, setValue] = React.useState('');
return (
<div>
<div>{props.q}</div>
<input value={value} onChange={(e) => setValue(e.target.value)}></input>
<button
onClick={() => {
props.onSaveAnswer(value);
}}
>
save answer
</button>
</div>
);
};
let QuestionNoSaveBtn = (props) => {
return (
<div>
<div>{props.q}</div>
<input
value={props.value || ''}
onChange={(e) => props.onSaveAnswer(e.target.value)}
></input>
</div>
);
};
export default function App() {
let [answers, setAnswers] = React.useState({});
console.log(answers);
return (
<div>
{questions.map((x) =>
x.type !== 'NoSaveBtn' ? (
<Question
q={x.q}
key={x.id}
onSaveAnswer={(answer) =>
setAnswers({ ...answers, [x.id]: answer })
}
/>
) : (
<QuestionNoSaveBtn
q={x.q}
key={x.id}
value={answers[x.id]}
onSaveAnswer={(answer) =>
setAnswers({ ...answers, [x.id]: answer })
}
/>
)
)}
</div>
);
}

Related

Trying to mutate the state of React component

I'm trying to create a quizz app and got stuck at the moment when I need to change the background of the Answers (button) when the user clicked on it. Well, function holdAnswer does console.log the id of the answer, but doesn't change its background. What's missing here?
I assume I have also stored all answers that the user chose in some array in order to count how many answers the user guessed.
After I check if the answers are correct or not they have to be highlighted accordingly (correct/incorrect), so again it needs to mutate the state.
Is the code missing something from the beginning?
here is CodeSandBox link
App.js
import { useState } from "react";
import QuestionSet from "./QuestionSet";
import Answers from "./Answers";
import { nanoid } from "nanoid";
function App() {
const [isQuesLoaded, setIsQuesLoaded] = useState(false);
const [questions, setQuestions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
async function startQuiz() {
try {
setIsQuesLoaded(!isQuesLoaded);
const response = await fetch(
"https://opentdb.com/api.php?amount=5&category=12&difficulty=easy&type=multiple"
);
const data = await response.json();
const allQuestions = data.results;
const listOfQuestions = allQuestions.map((item) => {
const allAnswers = [
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[0],
},
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[1],
},
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[2],
},
{
id: nanoid(),
isCorrect: true,
isChosen: false,
answer: item.correct_answer,
},
];
return {
id: nanoid(),
question: item.question,
answers: allAnswers,
};
});
setQuestions(listOfQuestions);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
function holdAnswer(id) {
console.log(id);
setQuestions((prevQuestion) =>
prevQuestion.map((question) =>
question.answers.id === id
? {
...question,
answers: question.answers.map((answer) =>
answer.id === id
? { ...answer, isChosen: !answer.isChosen }
: answer
),
}
: question
)
);
}
const questionElm = questions.map((question, index) => {
return (
<section key={index}>
<QuestionSet question={question.question} key={question.id} />
<Answers
answers={question.answers}
isChosen={question.answers.isChosen}
holdAnswer={holdAnswer}
/>
</section>
);
});
return (
<div className="App">
{!isQuesLoaded ? (
<main>
<h1 className="title-app">Quizzical</h1>
<p className="desc-app">Some description if needed</p>
<button className="btn" onClick={startQuiz}>
Start Quiz
</button>
</main>
) : (
<main className="quest-box">
{loading && <div>Loading data...</div>}
{error && <div>{`There is a problem fetchning data = ${error}`}</div>}
<section className="quest-content">{questionElm}</section>
<button className="answer-btn">Check Answers</button>
</main>
)}
</div>
);
}
export default App;
Answers.js
export default function Answers(props) {
const styles = {
backgroundColor: props.answers.isChosen ? "#D6DBF5" : "transparent",
};
return (
<section className="answer-container">
<div
className="answer-div"
style={styles}
id={props.answers[3].id}
onClick={() => props.holdAnswer(props.answers[3].id)}
>
<p>{props.answers[3].answer}</p>
</div>
<div
className="answer-div"
style={styles}
id={props.answers[1].id}
onClick={() => props.holdAnswer(props.answers[1].id)}
>
<p>{props.answers[1].answer}</p>
</div>
<div
className="answer-div"
style={styles}
id={props.answers[2].id}
onClick={() => props.holdAnswer(props.answers[2].id)}
>
<p>{props.answers[2].answer}</p>
</div>
<div
className="answer-div"
style={styles}
id={props.answers[0].id}
onClick={() => props.holdAnswer(props.answers[0].id)}
>
<p>{props.answers[0].answer}</p>
</div>
</section>
);
}
Your making confusions on object fields, using typescript will prevent you from doing this kind of error.
function holdAnswer(id) {
console.log(id);
setQuestions((prevQuestion) =>
prevQuestion.map((question) =>
question.answers.id === id // question.answers is an array
// you need to pass the questionId to holdAnswer and use it here
? {
...question,
answers: question.answers.map((answer) =>
answer.id === id
? { ...answer, isChosen: !answer.isChosen }
: answer
)
}
: question
)
);
}
// props.answers is also an array, create a component for Answer and use style here
const styles = {
backgroundColor: props.answers.isChosen ? "#D6DBF5" : "transparent"
};
Here is the codesandbox if you want to see how to fix it.
I wont give any direct answer, its up to you to find out.
First, there is an issue with the React.StrictMode as it runs some of the react api twice such as useEffect (and in ur case the setter for setQuestions), you can remove it for now since i dont think you really need it for this.
Lastly, if you look carefully at where you are changing the styles conditionally you'll see that you are referencing some object fields incorrectly.
it looks like you're just starting out react, so good luck and happy coding.

Getting first values from objects instead of getting that was chosen by user

Function holdAnswer has to change the property isChosen from false to true and mutate state, adding new value to the questions. Then function checkAnswers has to find answers that user has chosen and compare if the selected answers have property isCorrect, which has to be true. And if isCorrect is true and isChosen is true, function checkAnswers has to change the score count. It keeps showing me only the first answers, no matter what the user chose. Why it doesn't show me the answers which user chose?
Please take a look at Codesandbox
App.js
import { useState } from "react";
import QuestionSet from "./components/QuestionSet";
import Answers from "./components/Answers";
import { nanoid } from "nanoid";
function App() {
const [isQuesLoaded, setIsQuesLoaded] = useState(false);
const [questions, setQuestions] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [score, setScore] = useState(0);
async function startQuiz() {
try {
setIsQuesLoaded(!isQuesLoaded);
const response = await fetch(
"https://opentdb.com/api.php?amount=5&category=12&difficulty=easy&type=multiple"
);
const data = await response.json();
const allQuestions = data.results;
const listOfQuestions = allQuestions.map((item) => {
const allAnswers = [
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[0]
},
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[1]
},
{
id: nanoid(),
isCorrect: false,
isChosen: false,
answer: item.incorrect_answers[2]
},
{
id: nanoid(),
isCorrect: true,
isChosen: false,
answer: item.correct_answer
}
];
return {
id: nanoid(),
question: item.question,
answers: allAnswers
};
});
setQuestions(listOfQuestions);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
function holdAnswer(questionId, answerId) {
console.log({ questionId, answerId });
setQuestions((prevQuestion) =>
prevQuestion.map((question) =>
question.id === questionId
? {
...question,
answers: question.answers.map((answer) =>
answer.id === answerId
? { ...answer, isChosen: !answer.isChosen }
: answer
)
}
: question
)
);
}
function checkAnswers() {
let correctCount = 0;
questions.forEach(question => {
const selectedAnswer = question.answers.find(answer => answer.answer.id === answer.answer.isChosen);
if (selectedAnswer.isCorrect) correctCount += 1;
setScore(correctCount);
console.log(correctCount);
console.log(selectedAnswer);
})
}
const questionElm = questions.map((question, index) => {
return (
<section key={index}>
<QuestionSet question={question.question} key={question.id} />
<Answers
answers={question.answers}
isChosen={question.answers.isChosen}
holdAnswer={(answerId) => holdAnswer(question.id, answerId)}
/>
</section>
);
});
return (
<div className="App">
{!isQuesLoaded ? (
<main>
<h1 className="title-app">Quizzical</h1>
<p className="desc-app">Some description if needed</p>
<button className="btn" onClick={startQuiz}>
Start Quiz
</button>
</main>
) : (
<main className="quest-box">
{loading && <div>Loading data...</div>}
{error && <div>{`There is a problem fetchning data = ${error}`}</div>}
<section className="quest-content">{questionElm}</section>
<button className="answer-btn" onClick={checkAnswers}>Check Answers</button>
</main>
)}
</div>
);
}
export default App;
Answers.js
function Answer(props) {
const styles = {
backgroundColor: props.answer.isChosen ? "#D6DBF5" : "transparent"
};
return (
<div
className="answer-div"
style={styles}
id={props.answer.id}
onClick={() => props.holdAnswer(props.answer.id)}
>
<p>{props.answer.answer}</p>
</div>
);
}
export default function Answers(props) {
return (
<section className="answer-container">
{props.answers.map((answer) => (
<Answer holdAnswer={props.holdAnswer} answer={answer} key={answer.id} />
))}
</section>
);
}
QuestionSet.js
export default function QuestionSet(props) {
return (
<section className="quest" key={props.id}>
<p>{props.question}</p>
</section>
);
}
First thing that I'm seeing is that you are passing holdAnswer function down via props on a wrong way
You need to change to something this (Simplified for convenience):
App.js
<Answers
answers={question.answers}
isChosen={question.answers.isChosen}
holdAnswer={
holdAnswer(question.id, answerId) // Telling what to execute from here (Without the param on the arrow function)
}
/>
Answers.js
<Answer holdAnswer={
props.holdAnswer // passing the whole function knowing what to execute
}
/>
Answer
<button onClick={() =>
props.holdAnswer // knowing what to do from App.js
}
/>
Or you can do it like this:
App.js
<Answers
answers={question.answers}
isChosen={question.answers.isChosen}
holdAnswer={
holdAnswer // passing the whole function to execute it in another component
}
/>
Answers.js
<Answer holdAnswer={
props.holdAnswer // passing the whole function to execute it in another
}
/>
Answer
<button onClick={() =>
props.holdAnswer(props.param1, props.param2) // executing the function here passing the parameters
}
/>
Not sure if this is going to fix your problem but at least it can simplify it.

Incorrect validation when trying to send data from additional inputs

Hello everyone and thank you for reading this! Here is my problem that i can't solve:
My application has the following functionality:
There are 2 inputs, then a button, when clicked, 2 more inputs appear and a button to send data from all inputs to the console, however, in the additional field, one input is required. This is where my problem arises: now, if I called additional inputs and filled in all the data, they are transferred to the console, if I didn’t fill in the required field, an error message goes to the console, BUT. I also need, in the event that I did NOT call additional inputs, the data of 2 basic inputs was transferred to the console. At the moment I can't figure it out.
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import produce from "immer";
const FunctionalBlock = ({
id,
idx,
isDeleted,
toggleBlockState,
additionalValue,
additionalTitle,
setNewBlock,
index,
}) => {
return (
<div
style={{
display: "flex",
maxWidth: "300px",
justifyContent: "space-between",
}}
>
{!isDeleted ? (
<React.Fragment>
<strong>{idx}</strong>
<input
type="text"
value={additionalTitle}
onChange={(e) => {
const additionalTitle = e.target.value;
setNewBlock((currentForm) =>
produce(currentForm, (v) => {
v[index].additionalTitle = additionalTitle;
})
);
}}
/>
<input
type="text"
value={additionalValue}
onChange={(e) => {
const additionalValue = e.target.value;
setNewBlock((currentForm) =>
produce(currentForm, (v) => {
v[index].additionalValue = additionalValue;
})
);
}}
/>
<button onClick={toggleBlockState}>now delete me</button>
</React.Fragment>
) : (
<button onClick={toggleBlockState}>REVIVE BLOCK</button>
)}
</div>
);
};
const Application = () => {
const [newBlock, setNewBlock] = useState([]);
const [firstInput, setFirstInput] = useState("");
const [secondInput, setSecondInput] = useState("");
const getNewBlock = (idx) => ({
id: Date.now(),
idx,
isDeleted: false,
additionalValue: "",
additionalTitle: "",
});
const toggleIsDeletedById = (id, block) => {
if (id !== block.id) return block;
return {
...block,
isDeleted: !block.isDeleted,
};
};
const createOnClick = () => {
const block = getNewBlock(newBlock.length + 1);
setNewBlock([...newBlock, block]);
};
const toggleBlockStateById = (id) => {
setNewBlock(newBlock.map((block) => toggleIsDeletedById(id, block)));
};
const showInputData = () => {
newBlock.map((item) => {
if (item.additionalTitle.length < 3) {
console.log("it is less than 3");
} else if (!item.additionalTitle && !item.additionalValue) {
console.log(firstInput, secondInput);
} else {
console.log(
firstInput,
secondInput,
item.additionalTitle,
item.additionalValue
);
}
});
};
return (
<div>
<div>
<input
type="text"
value={firstInput}
onChange={(e) => {
setFirstInput(e.target.value);
}}
/>
<input
type="text"
value={secondInput}
onChange={(e) => {
setSecondInput(e.target.value);
}}
/>
</div>
<div>
<button onClick={createOnClick}>ADD NEW INPUTS</button>
</div>
<div>
{newBlock.map((block, index) => (
<FunctionalBlock
key={index}
{...block}
toggleBlockState={() => toggleBlockStateById(block.id)}
setNewBlock={setNewBlock}
index={index}
/>
))}
</div>
<button onClick={showInputData}>send data</button>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Application />);
Here is this code on sandbox for those who decided to help me. Thank you!
https://codesandbox.io/s/vigilant-booth-xnef6t

How can I conditionally disable an input selection using a prop?

When a restart button is clicked on my quiz, I want the input selections to be removed. I tried testing disabled={restart === true} in my input selection on the question component, but it's not exactly working correctly. Any clue how to accomplish this? https://replit.com/#arshia93/Quizzical#sections/Question.jsx
Question component
export default function Question({question, answers, correctAnswer, updateAnswers, restart}) {
return (
<>
<h3 className="questions">
{`${decode(question)}`}
</h3>
{answers.map(( answerOption, index ) => (
<div key={index}>
<input
disabled={restart === true}
type="radio"
name={`answer option-${question}`}
id={`answer-options-${answerOption}`}
value={answerOption}
onChange={updateAnswers}
/>
<label
className={`answer-options ${correctAnswer ? 'answer-correct' : ``}`}
htmlFor={`answer-options-${question}`}>
{`${decode(answerOption)}`}</label>
</div>
))}
</>
)
}
Quiz data component
export default function QuizData() {
const [finished, setFinished] = React.useState(false)
const [newQuiz, setNewQuiz] = React.useState(false)
const [quizData, setQuizData] = React.useState([{
question: "",
answers: "",
correctAnswer: ""
}]);
const [selections, setSelections] = React.useState([]);
const [score, setScore] = React.useState(0)
React.useEffect(() => {
fetch("https://opentdb.com/api.php?amount=5&category=12&difficulty=medium&type=multiple")
.then(res => res.json())
.then(data => setQuizData(data.results.map(item => ({
question: item.question,
answers: arrayShuffle(item.incorrect_answers.concat(item.correct_answer)),
correctAnswer: item.correct_answer
}))))
},[newQuiz])
function handleSelectedAnswer(event) {
const {value} = event.target
setSelections(prevData => prevData.length > 0 ? [...prevData, value] : [value])
}
const allCorrectAnswers = quizData.map(item => item.correctAnswer)
const totalScore = selections.reduce((score, selection) =>
score + (allCorrectAnswers.includes(selection) ? 1 : 0) , 0);
function complete() {
setScore(totalScore)
setFinished(true)
}
function restart() {
setFinished(false)
setNewQuiz(prevState => !prevState)
}
return (
<div>
{ quizData.length > 1 ?
(quizData.map((item, index) => (
<div>
<Question
key={index}
question={item.question}
answers={item.answers}
restart={newQuiz}
correctAnswer={finished === true ? item.correctAnswer : ""}
chosenAnswer={selections.selectedAnswer}
updateAnswers={handleSelectedAnswer}
/>
</div>
)))
: <p>Loading...</p>
}
{finished === false ?
<button
className="quiz-button"
onClick={complete}>
Check answers
</button>
:
<>
{`You scored ${score}/5 correct`}
<button
className="quiz-button"
onClick={restart}>
Play again
</button>
</>
}
</div>
)
}`
Closed the earlier thread, because might be missleading.
In fact (wtihtout the simplifications and clearing) all you need to do is just remove the
value={answerOption}
disabled={restart === true}
line from Question.jsx (line 18 and 15).
You dont need to change the value dynamically here, since you always get new questions (thus no answers to display at the start) + no reason to disable it
add the key attribute for proper updates for the Question.jsx (Line 14)
key={answerOption}

todos.map is not a function - not able to edit a specific item

i'm trying to work on a todo app with the option of editing.
the goal is to click on the edit button and that'll open an input field, type the new editted text and then have two choices , save the changes or not.
i've managed to write the code for it to open the input field, and to be able to click on the button to not save changes ,but what happens is that it opens the input field for all of the todos ,and whenever i try to update the value of the specific todo i get the error "todos.map is not a function".
Here's the TodoList.js
import Todo from "./Todo";
import AddTodo from "./AddTodo";
import { v4 as uuidv4 } from "uuid";
const TodoList = () => {
//Handlers Add/Remove/RemoveAll/Edit
const addTodoHandler = (input) => {
setTodos([
...todos,
{
name: input,
id: uuidv4(),
},
]);
};
const changeEditMode = (id) => {
setEditMode(!editMode);
console.log(id);
};
const removeTodosHandler = () => {
if (window.confirm("Are you sure you want to delete everything?")) {
setTodos([]);
}
};
const removeTodoHandler = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
const updateValue = (id) => {
inputRef.current.focus();
setEditMode(!editMode);
setTodos({ name: inputRef.current.value });
};
//Todo list states.
const inputRef = useRef("");
const [todos, setTodos] = useState([]);
const [editMode, setEditMode] = useState(false);
return (
<div>
<div>
{todos.map((todo) => {
return (
<div>
{editMode ? (
<div>
{" "}
<input
type="text"
defaultValue={todo.name}
ref={inputRef}
></input>
<button onClick={(e) => updateValue(todo.id)}>ok</button>
<button onClick={(e) => setEditMode(!editMode)}>x</button>
</div>
) : (
<div></div>
)}
<Todo name={todo.name} key={todo.id} />
<button onClick={() => removeTodoHandler(todo.id)}>X</button>
<button onClick={(e) => changeEditMode(todo.id)}>Edit</button>
</div>
);
})}
</div>
<AddTodo
handleAddTodo={addTodoHandler}
removeTodosHandler={removeTodosHandler}
revemoveTodoHandler={removeTodoHandler}
/>
</div>
);
};
export default TodoList;
and here's the Todo.js
const Todo = ({ name }) => {
return (
<div>
<div>{name}</div>
</div>
);
};
export default Todo;
Any help appreciated!
Your updateValue function is setting your todos to an object. So it gives you that error because you can't use map method for objects.
In your updateValue method you are setting your todoList to and object.
const updateValue = (id) => {
inputRef.current.focus();
setEditMode(!editMode);
setTodos({ name: inputRef.current.value });
};
But what you have to do is first find out the item with the id and then update the name property of that object and then again set the new array to setTodos setter.
Like this:
const clonedTodos = todos;
const todoIndex = clonedTodos.findIndex((todo) => todo.id === id);
const updatedTodo = {
...clonedTodos[todoIndex],
name: inputRef.current.value,
};
const updatedTodos = [...clonedTodos];
updatedTodos[todoIndex] = updatedTodo;
setTodos(updatedTodos);

Categories

Resources