I'm trying to create a quiz to return the data based on selected checkboxes, the best option I find to do it is creating an array with index of every selected items but when I try to render page I receive the error of too many re-renders.
I have a JSON with some questions and the answers and I'm tryng to retrieve the list index of all selected answers and after compare with my JSON to return the right answer.
import "./questions.scss";
import React, { useState, useEffect, useRef } from "react";
import { HashLink as Link } from "react-router-hash-link";
export default function Questions({ data }) {
const [error, setError] = useState("");
const radiosWrapper = useRef();
const [updateCheckedState, setUpdateCheckedState] = useState([]);
const [temperament, SetTemperament] = useState("");
console.log(updateCheckedState);
useEffect(() => {
radiosWrapper.current.querySelectorAll("input:checked").forEach((el) => (el.checked = false));
}, [data]);
const changeHandler = (position) => {
if (!updateCheckedState.includes(position)) {
setUpdateCheckedState((arr) => [...arr, position]);
} else if (updateCheckedState.includes(position)) {
const index = updateCheckedState.indexOf(position);
setUpdateCheckedState(updateCheckedState.splice(index, 1));
}
const sanguine = data.sanguine.filter((e) => updateCheckedState.includes(e));
const choleric = data.choleric.filter((e) => updateCheckedState.includes(e));
const melancholic = data.melancholic.filter((e) => updateCheckedState.includes(e));
const phlegmatic = data.phlegmatic.filter((e) => updateCheckedState.includes(e));
const result = Math.max(sanguine.length, choleric.length, melancholic.length, phlegmatic.length);
switch (result) {
case sanguine.length:
SetTemperament("/sanguine");
break;
case choleric.length:
SetTemperament("/choleric");
break;
case melancholic.length:
SetTemperament("/melancholic");
break;
case phlegmatic.length:
SetTemperament("/phlegmatic");
break;
default:
}
if (error) {
setError("");
}
};
return (
<div className="card">
<div className="card-content">
<div className="content">
<h2 className="mb-5">{data.question}</h2>
<div className="control" ref={radiosWrapper}>
{data.choices.answer.map((choice, i) => (
<label className="radio has-background-light" key={i}>
<input type="checkbox" name="answer" value={choice} onChange={changeHandler(i)} />
{choice}
</label>
))}
</div>
{error && <div className="has-text-danger">{error}</div>}
<Link className="link" to={temperament}>
Resultado
</Link>
</div>
</div>
</div>
);
}
CodePudding user response:
The problem is on the following line:
<input type="checkbox" name="answer" value={choice} onChange={changeHandler(i)} />
You are invoking the function instead you should pass in a function as shown below.
<input type="checkbox" name="answer" value={choice} onChange={() => changeHandler(i)} />
CodePudding user response:
The rerender bug is caused, like Som Shekhar Mukherjee pointed out, by the fact you're calling changeHandler
during render, since it's not wrapped in a function.
Your code could be simplified a bunch, though, by noting that the temperament
is a pure derivation of checked
and data
, i.e. a prime candidate for useMemo
.
Also, it's better to use the checked
prop for the checkboxes, so your state can never be out of sync with what the browser is showing:
import "./questions.scss";
import React, { useState, useEffect } from "react";
import { HashLink as Link } from "react-router-hash-link";
function computeTemperament(data, checked) {
const temperamentCounts = {};
// Count the number of times each temperament is checked
["sanguine", "phlegmatic", "melancholic", "choleric"].forEach((temperament) => {
temperamentCounts[temperament] = data[temperament].filter(e => checked.includes(e)).length;
});
// Find the temperament with the highest count
return Object.keys(temperamentCounts).reduce((a, b) =>
temperamentCounts[a] > temperamentCounts[b] ? a : b
);
}
export default function Questions({ data }) {
const [checked, setChecked] = useState([]);
useEffect(() => {
setChecked([]);
}, [data]);
const changeHandler = React.useCallback((position) => {
if (!checked.includes(position)) {
setChecked((checked) => [...checked, position]);
} else {
setChecked((checked) => checked.filter((item) => item !== position));
}
}, []);
const temperament = React.useMemo(() => computeTemperament(data, checked), [data, checked]);
return (
<div className="card">
<div className="card-content">
<div className="content">
<h2 className="mb-5">{data.question}</h2>
<div className="control">
{data.choices.answer.map((choice, i) => (
<label className="radio has-background-light" key={i}>
<input
type="checkbox"
name="answer"
value={choice}
checked={checked.includes(i)}
onChange={() => changeHandler(i)}
/>
{choice}
</label>
))}
</div>
<Link className="link" to={temperament}>
Resultado
</Link>
</div>
</div>
</div>
);
}