Home > Back-end >  React component props not changing when they should?
React component props not changing when they should?

Time:02-22

A project I am working on involves a large component tree passing props down which come from a large complex state object in the top component.

In one particular component "ChecklistEditor" I am mapping the "subchecklists" prop into "Subchecklist" components, each getting their respective props. The problem I am finding is that when I update the state of a subchecklist (in parent component), it causes ChecklistEditor's props (subchecklists) to change (as I see from devtools), but even though these props are passed to the Subchecklist components in the map function, the props for the Subchecklist component do not change and it doesnt re-render with the updated data.

ChecklistEditor.js

import { useState, useCallback } from "react";

import ChecklistTitle from "../ChecklistTitle/ChecklistTitle";
import Subchecklist from "../Subchecklist/Subchecklist";
import NewSubchecklistForm from "../NewSubchecklistForm/NewSubchecklistForm";
import BlankSpace from "../BlankSpace/BlankSpace";
import Button from "../../UI/Button";
import classes from "./ChecklistEditor.module.css";
import useMemoizedCallback from "../../../hooks/useMemoizedCallback";
import SectionTitle from "../SectionTitle/SectionTitle";

const ChecklistEditor = (props) => {
  const [dragItemIndex, setDragItemIndex] = useState(-1);
  const [draggedOverItemIndex, setDraggedOverItemIndex] = useState(-1);

  // Called when the subchecklist is dragged.
  const handleDrag = useCallback(
    (subchecklistIndex) => {
      setDragItemIndex(subchecklistIndex);
    },
    [setDragItemIndex]
  );

  // Called when another subchecklist is dragged over this subchecklist.
  const handleDragOver = useCallback(
    (itemIndex) => {
      setDraggedOverItemIndex(itemIndex);
    },
    [setDraggedOverItemIndex]
  );

  // Called when the dragend event fires for a subchecklist. (Memoized so that it doesnt cause tons of re-renders).
  const handleDragEnd = useMemoizedCallback(() => {
    props.onReorderSubchecklists(dragItemIndex, draggedOverItemIndex);
  }, [props.onReorderSubchecklists, dragItemIndex, draggedOverItemIndex]);

  // Resets the index of the subchecklist being dragged.
  const resetDragItemIndex = useCallback(() => {
    setDragItemIndex(-1);
  }, [setDragItemIndex]);

  // Map item data into Subchecklist elements
  const subchecklists = props.subchecklists.map((item, index) => {
    let returnItem;
    if (item.type === "subchecklist") {
      returnItem = (
        <Subchecklist
          key={item.id}
          subchecklistIndex={index}
          subchecklistId={item.id}
          title={item.title}
          color={item.color}
          items={item.checkItems}
          onDeleteSubchecklist={props.onDeleteSubchecklist}
          onUpdateSubchecklistTitle={props.onUpdateSubchecklistTitle}
          onChangeColor={props.onChangeSubchecklistColor}
          onItemUpdate={props.onItemUpdate}
          onDeleteItem={props.onDeleteItem}
          onAddCheckItem={props.onAddCheckItem}
          onAddCondition={props.onAddCondition}
          onDrag={handleDrag}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
          onReorderCheckItems={props.onReorderCheckItems}
          resetDragItemIndex={resetDragItemIndex}
        />
      );
    } else if (item.type === "blankSpace") {
      returnItem = (
        <BlankSpace
          key={item.id}
          id={item.id}
          index={index}
          height={item.height}
          onUpdateHeight={props.onUpdateBlankSpace}
          onDelete={props.onDeleteSubchecklist}
          onDrag={handleDrag}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
          onReorderCheckItems={props.onReorderCheckItems}
          resetDragItemIndex={resetDragItemIndex}
        />
      );
    } else if (item.type === "sectionTitle") {
      returnItem = (
        <SectionTitle
          key={item.id}
          id={item.id}
          index={index}
          title={item.title}
          color={item.color}
          onUpdateTitle={props.onUpdateSectionTitle}
          onChangeColor={props.onChangeSubchecklistColor}
          onDelete={props.onDeleteSubchecklist}
          onDrag={handleDrag}
          onDragOver={handleDragOver}
          onDragEnd={handleDragEnd}
          onReorderCheckItems={props.onReorderCheckItems}
          resetDragItemIndex={resetDragItemIndex}
        />
      );
    }
    return returnItem;
  });

  return (
    <div className={classes.container}>
      <div className={classes.editor}>
        <ChecklistTitle
          title={props.name}
          onTitleChange={props.onTitleChange}
        />
        <Button type="submit" onClick={props.onSave}>
          Save
        </Button>
        <div className={classes.subchecklistContainer}>{subchecklists}</div>
        <NewSubchecklistForm onSubmit={props.onAddSubchecklist} />
        <Button type="submit" onClick={props.onAddBlankSpace}>
          Add Blank Space
        </Button>
        <Button type="submit" onClick={props.onAddSectionTitle}>
          Add Section Title
        </Button>
      </div>
    </div>
  );
};

export default ChecklistEditor;

Something weird that I found was that if I make the change to the subchecklist state, and then make a small change in my code and save, nodemon will reload the app and the values will update to what they should be.

I am really stuck on this and have no idea what is going on, so I will appreciate any help I can get. Thank you.

EDIT: I should mention that if I remove the useCallback hook from any of the functions I am passing into the Subchecklist component, that it forces the component to reload and the information is updated correctly, but this becomes too expensive with re-renders. This also seems wrong because the function changing is whats causing the reload when the data prop itself should be causing it...

CodePudding user response:

I'm making assumptions here about the rest of your code, but I feel that it is likely that in this part:

const handleDragEnd = useMemoizedCallback(() => {
  props.onReorderSubchecklists(dragItemIndex, draggedOverItemIndex);
}, [props.onReorderSubchecklists, dragItemIndex, draggedOverItemIndex]);

You are actually modifying the state directly, instead of using setDragItemIndex and setDraggedOverItemIndex. Which will cause problems with it rendering correctly. If I am wrong, then all I can guess is that somewhere else you are making the same mistake somewhere else.

CodePudding user response:

Ok so I found the problem. It has to do with how the spread operator works when copying a nested state object.

In short; if a nested object in your state is used as a prop for a child component, just using the spread operator on the entire object will not suffice to make a copy of the nested properties. This is because the spread operator creates a shallow copy (one layer) and the nested objects will still reference the same objects as before and your props will not update to trigger a re-render.

So if my state object is something like shown below, if I map the contents array into multiple child components and want to have their changes show up in props and trigger a re-render of the component I will have to make a copy of the contents array to modify and then replace the old one with that new value.

const [myObj, setMyObj] = useState({
  name: "MyName",
  id: 1,
  contents: [
    {
      id: 2,
      name: "Nested Name"
    }
  ]
});

Wrong

let newMyObj = {...myObj};
newMyObj.contents[0].name = "Updated Name";
setMyObj(newMyObj);

Right

let newContents = [...myObj.contents];
newContents[0].name = "Updated Name";
let newMyObj = {...myObj};
newMyObj.contents = newContents;
setMyObj(newMyObj);
  • Related