Problem:
I’ve seen some solutions for other frameworks but none apply to me. I have two main issues here.
1- I have to click the button twice to get it to fire. Granted I am using the click even’t to mutate the state, change the row color, AND also update a list on the bottom of the table. I don’t think none of these are running asynchronously. So maybe that’s the problem and I would like to know how to go around that.
2- Second problem is related to the first. I passed the dependency in the useEffect function so that the list on the bottom is updated as soon as the click event changes the state. But it’s throwing an infinite loop, as if the state was constantly changing. A console log doesn’t reflect the state forever changing. So, I’m stooped.
Some basically I have an array (API in real world), and I’m filtering out the learned and unlearned questions
const questions = [
{
question: "Question 1",
answer: "My answer 1",
learned: true
},
{
question: "Question 2",
answer: "My answer 2",
learned: false
},
{
question: "Question 3",
answer: "My answer 3",
learned: true
},
{
question: "Question 4",
answer: "My answer 4",
learned: true
},
{
question: "Question 5",
answer: "My answer 5",
learned: false
},
{
question: "Question 6",
answer: "My answer 6",
learned: true
}
]
const learned = questions.filter(question => question.learned === true)
const unlearned = questions.filter(question => question.learned === false)
Inside my component I have the state, the function to populate a list, useEffect, and a click event function that contains the callback to populate the list, as well as toggling the UI from learned to unlearned, which of course, it updates the list accordingly
const [unlearned, setUnlearned] = useState([]);
const populateUnlearned = () => {
setUnlearned(questions.filter(question => question.learned === false))
}
useEffect(() => {
populateUnlearned()
},[]) //unlearned here causes the list to populate fine but an infinite loop
const handleLearned = (index) => {
populateUnlearned()
setIsLearned(
!isLearned,
questions.filter(question => question.learned === false),
questions[index].learned = !isLearned
)
}
And the rest is just html stuff. I have a sandbox here for you to mess with showing the problem.
I appreciate any help. Thanks in advance
Solution:
here are a few updates I would recommend,
- State variables
In Vocabulary.js, you attempt to modify the entries in questions
. However, this variable is not a piece of state. Keep in mind that only variables created based on useState
(and useReducer
) can be updated within your component
- Avoid performing updates based on index
here you can find information that explains in detail why you should not be using the index
// rename questions to original_questions
const original_questions = [
{
...
// compute them within the component
//const learned = questions.filter((question) => question.learned === true);
//const unlearned = questions.filter((question) => question.learned === false);
const Vocabulary = () => {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
// unneeded since each entry in "questions" keeps track that flag
// const [isLearned, setIsLearned] = useState(false);
// questions get initiated as a piece of state
// based on the original_questions (this can be a prop)
const [questions, setQuestions] = useState(original_questions);
const [unlearned, setUnlearned] = useState([]);
// effects computes unlearned
// whenever questions state changes
useEffect(() => {
populateUnlearned();
}, [questions]);
// other functions...
// instead of index,
// handleLearned expects the row data
// (ideally row should have a unique id e.g row.id)
// the update uses the `question` property to uniquely identify
// the question to update
const handleLearned = (row) => {
setQuestions((currentQuestions) =>
currentQuestions.map((entry) => {
if (row.question === entry.question) {
entry.learned = !entry.learned;
}
return entry;
})
);
//populateUnlearned();
//setIsLearned(
// !isLearned,
// questions.filter((question) => question.learned === false),
// (questions[index].learned = !isLearned)
//);
// setUnlearned(questions.filter(question => question.learned === false))
// questions[index].learned = !isLearned
};
// more code ...
// button onClick callback now gets the row instead of index
<StyledTableCell align="center">
<Button
onClick={() => handleLearned(row)}
variant="contained"
color={row.learned ? "error" : "success"}
size="small"
sx={{ mr: 2 }}
>
{row.learned ? "No" : "Yes"}
</Button>
</StyledTableCell>