skip to Main Content

In the Student (children) component

  • when the value of the variable changes, the useEffect hooks will update the parents’s array by handleStudentsChange, 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 the useCallback 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


  1. Problem

    handleStudentsChange runs infinitely once a change has occur

    handleStudentsChange doesn’t only run infinitely once a change occurs – it runs infinitely from the first render. This is because the Student component has a useEffect that calls handleStudentsChange which updates the state in the Students component causing Student components to rerender which then calls the useEffect 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 in Students after the blur 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.

    const { Fragment, StrictMode, useCallback, useEffect, useState } = React;
    const { createRoot } = ReactDOM;
    const { TextField } = MaterialUI;
    
    function Student(props) {
      const [firstName, setFirstName] = useState(props.firstName);
      const [lastName, setLastName] = useState(props.lastName);
      const [grade, setGrade] = useState(props.grade);
      const handleStudentsChange = props.handleStudentsChange;
      
      const onBlur = () => {
        handleStudentsChange(props.id, {
          firstName,
          lastName,
          grade,
        });
      };
    
      return (
        <Fragment>
          <TextField
            label="firstName"
            onBlur={onBlur}
            onChange={(event) => setFirstName(event.target.value)}
            value={firstName}
          />
          <TextField
            label="lastName"
            onBlur={onBlur}
            onChange={(event) => setLastName(event.target.value)}
            value={lastName}
          />
          <TextField
            label="grade"
            onBlur={onBlur}
            onChange={(event) => setGrade(+event.target.value)}
            value={grade}
          />
        </Fragment>
      );
    }
    
    function Students() {
      const [students, setStudents] = useState([
        { firstName: "Justin", lastName: "Bieber", grade: 100 },
        { firstName: "Robert", lastName: "Oppenhiemer", grade: 100 }
      ]);
    
      const handleStudentsChange = useCallback(
        (index, updatedStudent) => {
          // console.log(index) // I only want this to rerender when the value change however it turn into an infinity loop
          
          console.log({ updatedStudent });
    
          setStudents((prevStudents) => {
            const updatedStudents = [...prevStudents];
            updatedStudents[index] = updatedStudent;
            return updatedStudents;
          });
        },
        []
      );
    
      return (
        <Fragment>
          {students.map((student, index) => {
            return (
              <Student
                key={index}
                id={index}
                firstName={student.firstName}
                lastName={student.lastName}
                grade={student.grade}
                handleStudentsChange={(index, newStudent) =>
                  handleStudentsChange(index, newStudent)
                }
              />
            );
          })}
        </Fragment>
      );
    }
    
    function App() {
      return (
        <div className="App">
          <Students />
        </div>
      );
    }
    
    const root = createRoot(document.getElementById("root"));
    root.render(<StrictMode><App /></StrictMode>);
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <script crossorigin src="https://unpkg.com/@mui/material@latest/umd/material-ui.production.min.js"></script>
    <div id="root"></div>
    Login or Signup to reply.
  2. 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 the useState 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.

    function Student(props: studentProps) {
      const { firstName, lastName, grade, handleStudentsChange } = props;
    
      const handle = (name, value) => {
        handleStudentsChange({
          firstName,
          lastName,
          grade,
          [name]: value,
        });
      }
    
      return (
        <>
          <TextField
            label="firstName"
            onChange={(event) => handle("firstName", event.target.value)}
            value={firstName}
          />
          <TextField
            label="lastName"
            onChange={(event) => handle("lastName", event.target.value)}
            value={lastName}
          />
          <TextField
            label="grade"
            onChange={(event) => handle("grade", +event.target.value)}
            value={grade}
          />
        </>
      );
    }
    

    Then, modify the parent component accordingly:

      const handleStudentsChange = (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={(newStudent: student) =>
                  handleStudentsChange(index, newStudent)
                }
              />
            );
          })}
        </>
      );
    }
    

    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:

      return students.map((student, index) => (
              <Student
                key={index}
                id={index}
                {...student}
                handleStudentsChange={(newStudent: student) =>
                  handleStudentsChange(index, newStudent)
                }
              />
            ));
    

    NOTE: I didn’t test the code.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search