In the Student (children) component
- when the value of the variable changes, the
useEffect
hooks will update the parents’s array byhandleStudentsChange
, a function provided by the parent component.
In the Students (parent) component
- renders a list of Student(children) components
- In attempt to prevent infinity loop,
handleStudentsChange
function defined using theuseCallback
hook. However, it does not seem to be working.
Problems/Questions
handleStudentsChange
runs infinitely once a change has occur- Why is that? and How do I fix it?
- Note: I do not want a onSubmit button
See code here:
I am a CodeSandBox Link
Student.tsx (children)
import React, { useState, useEffect, useRef } from "react";
import TextField from "@mui/material/TextField";
interface student {
firstName: string;
lastName: string;
grade: number;
}
interface studentProps {
id: number;
firstName: string;
lastName: string;
grade: number;
handleStudentsChange: (index: number, student: student) => void;
}
function Student(props: studentProps) {
const [firstName, setFirstName] = useState(props.firstName);
const [lastName, setLastName] = useState(props.lastName);
const [grade, setGrade] = useState(props.grade);
useEffect(() => {
handleStudentsChange(id, {
firstName: firstName,
lastName: lastName,
grade: grade
});
}, [firstName, lastName, grade, props]);
return (
<>
<TextField
label="firstName"
onChange={(event) => setFirstName(event.target.value)}
value={firstName}
/>
<TextField
label="lastName"
onChange={(event) => setLastName(event.target.value)}
value={lastName}
/>
<TextField
label="grade"
onChange={(event) => setGrade(+event.target.value)}
value={grade}
/>
</>
);
Students.tsx (parent)
import React, { useState, useCallback } from "react";
import Student from "./Student";
interface student {
firstName: string;
lastName: string;
grade: number;
}
export default function Students() {
const [students, setStudents] = useState<student[]>([
{ firstName: "Justin", lastName: "Bieber", grade: 100 },
{ firstName: "Robert", lastName: "Oppenhiemer", grade: 100 }
]);
const handleStudentsChange = useCallback(
(index: number, updatedStudent: student) => {
// console.log(index) //I only want this to rerender when the value change however it turn into an infinity loop
setStudents((prevStudents) => {
const updatedStudents = [...prevStudents];
updatedStudents[index] = updatedStudent;
return updatedStudents;
});
},
[]
);
return (
<>
{students.map((student, index) => {
return (
<Student
key={index}
id={index}
firstName={student.firstName}
lastName={student.lastName}
grade={student.grade}
handleStudentsChange={(index: number, newStudent: student) =>
handleStudentsChange(index, newStudent)
}
/>
);
})}
</>
);
}
As shown in the code above, I tried using React.memo
on the student (children) component and useCallback
on handleStudentsChange
expecting the infinity loop will be prevented. However, the infinity loop continue.
2
Answers
Problem
handleStudentsChange
doesn’t only run infinitely once a change occurs – it runs infinitely from the first render. This is because theStudent
component has auseEffect
that callshandleStudentsChange
which updates the state in theStudents
component causingStudent
components to rerender which then calls theuseEffect
again, ad infinitum.Solution
You need
handleStudentsChange
to be called only after your inputs have been updated – not after every render. I’ve included an example below which updates the state inStudents
after theblur
event is fired from the input. For more cleverness (and complexity) you could diff the props and state to decide if an update is required but I’ll leave that to you to figure out.To me, yours seems an overcomplicated (yet instable) solution.
First off, you want the
Students
component be the "master" of the dataset and that’s a good choice. Hence, there’s no reason to replicate the state in the Student component: remove theuseState
and straight use the parent one.Secondly, it has no sense to trigger a value change when there’s no change. If you use the
useEffect
in that way, it will trigger an handler call even on the component load, thus no change.Then, modify the parent component accordingly:
As you may notice, there’s no need of
useCallback
.I don’t get what’s the role of the "id" property.
The latter snippet could be written in the following more compact way:
NOTE: I didn’t test the code.