react-nativereact-hooksmaterial-ui

React checkbox gets stuck when updating parent state


I'm new to React, I'm trying to write a component that hold different groups, each group have accumulated amount of points and each group hold different checkboxes, I want that when the checkbox is clicked the amount will update in the correct way (add/subtract depend on the checkbox state)

Here is my code where I tried to check if its working(I've omitted unrelated code and includes):

export default function Tracker() {

 
const [amountMust, setAmountMust] = React.useState(0); // 84
const [amountListA, setAmountListA] = React.useState(0); // 24.5
const [amountMalag, setAmountMalag] = React.useState(0); //6
const [amountSport, setAmountSport] = React.useState(0); //2
const [amountScience, setAmountScience] = React.useState(0); // 8
const [amountGeneral, setAmountGeneral] = React.useState(0); // 2
const [amountProject, setAmountProject] = React.useState(0); // 2
const [amountEnglish, setAmountEnglish] = React.useState(0); // 2


const courses = [
      { number: "101", name: "Mathematics", points: 3 },
      { number: "102", name: "Physics", points: 4 },
      { number: "103", name: "Chemistry", points: 2 },
    ];

    const CheckBoxState = ({ points, groupSetter }) => {
      const [isChecked, setIsChecked] = React.useState(false);
  
      const handleCheckboxChange = (e) => {
            const newChecked = e.target.checked;
            setIsChecked(newChecked);       
            groupSetter ((prevGroupPoints) =>
            newChecked ? prevGroupPoints + points : prevGroupPoints - points);      
      };
  
      return (
        <Checkbox
          variant="soft"
          size="md"
          color="success"
          checked={isChecked}
          onChange={handleCheckboxChange}
        />
      );
    };

    const CourseList = ({ courses }) => {
      return (
            
        <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
          {courses.map((course) => (
            <FormControl
              sx={{
                display: "flex",
                flexDirection: "row",
                gap: 1,
                border: "1px solid #ccc",
                borderRadius: 2,
                padding: 2,
              }}
            >
              <Box sx={{ display: "fixed", alignItems: "center", gap: 2 }}>
                <CheckBoxState points={course.points} groupSetter={setAmountMust}/>
                <Typography variant="body2">{course.number}</Typography> 
                <Typography variant="body2">{course.name}</Typography>
                <Typography variant="body2">נק"ז: {course.points}</Typography>
            </Box>
              <Box sx={{ display: "flex", gap: 2, mt: 2, marginLeft: 1 }}>
                <FormControl>
                  <FormLabel>Option 1</FormLabel>
                  <Switch size="sm" />
                </FormControl>
                <FormControl>
                  <FormLabel>Option 2</FormLabel>
                  <Switch size="sm" />
                </FormControl>
              </Box>
            </FormControl>
          ))}
        </Box>
      );
    };


  return (
      <div className='box'>
            <CourseList />
      </div>

  );
}

the problem is when calling groupSetter inside handleCheckboxChange the checkbox state does not change and keeps unchecked, and the value is added to amountMust over and over again at each click, but when removing groupSetter or passing it a regular value that does not depend on the previous state it works.

The purpose of passing the state setter and value to the checkbox component is because I want to use this component for different groups in the code.


Solution

  • Don't declare React components inside other React components. The problem here is that each time the state updates in Tracker, both CheckBoxState and CourseList are re-declared and are new React component references. React will unmount the previous "versions/instances" and mount the new "versions/instances". Any state and UI they had will be reset as well.

    Move these component declarations outside the Tracker component declaration and pass in the props they need instead of relying on the Javascript closure the Tracker function provides.

    Example:

    const courses = [
      { number: "101", name: "Mathematics", points: 3 },
      { number: "102", name: "Physics", points: 4 },
      { number: "103", name: "Chemistry", points: 2 },
    ];
    
    const CheckBoxState = ({ points, setAmountMust }) => {
      const [isChecked, setIsChecked] = React.useState(false);
      
      const handleCheckboxChange = (e) => {
        const { checked } = e.target;
        setIsChecked(checked);       
        setAmountMust((amountMust) => checked
          ? amountMust + points
          : amountMust - points
        );
      };
      
      return (
        <Checkbox
          variant="soft"
          size="md"
          color="success"
          checked={isChecked}
          onChange={handleCheckboxChange}
        />
      );
    };
    
    const CourseList = ({ courses, setAmountMust }) => {
      return (
        <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
          {courses.map((course) => (
            <FormControl
              key={course.name}
              sx={{
                display: "flex",
                flexDirection: "row",
                gap: 1,
                border: "1px solid #ccc",
                borderRadius: 2,
                padding: 2,
              }}
            >
              <Box sx={{ display: "fixed", alignItems: "center", gap: 2 }}>
                <CheckBoxState
                  points={course.points}
                  setAmountMust={setAmountMust}
                />
                <Typography variant="body2">{course.number}</Typography> 
                <Typography variant="body2">{course.name}</Typography>
                <Typography variant="body2">נק"ז: {course.points}</Typography>
            </Box>
              <Box sx={{ display: "flex", gap: 2, mt: 2, marginLeft: 1 }}>
                <FormControl>
                  <FormLabel>Option 1</FormLabel>
                  <Switch size="sm" />
                </FormControl>
                <FormControl>
                  <FormLabel>Option 2</FormLabel>
                  <Switch size="sm" />
                </FormControl>
              </Box>
            </FormControl>
          ))}
        </Box>
      );
    };
    
    export default function Tracker() {
      const [amountMust, setAmountMust] = React.useState(0); // 84
      const [amountListA, setAmountListA] = React.useState(0); // 24.5
      const [amountMalag, setAmountMalag] = React.useState(0); //6
      const [amountSport, setAmountSport] = React.useState(0); //2
      const [amountScience, setAmountScience] = React.useState(0); // 8
      const [amountGeneral, setAmountGeneral] = React.useState(0); // 2
      const [amountProject, setAmountProject] = React.useState(0); // 2
      const [amountEnglish, setAmountEnglish] = React.useState(0); // 2
    
      return (
        <div className='box'>
          <CourseList
            courses={courses}
            setAmountMust={setAmountMust}
          />
        </div>
      );
    }