0

So, I was refactoring a React component after reading more about React principles of lifting state up and in particular after reading this about derived state.

I had a component like so (before the refactor) that was working but now I realize was duplicating state among multiple sources of truth (in the parent and the child):

const ChildComponent = ({
  prevStep,
  nextStep,
  parentStateData,
  updateParentStateData,
}) => {
  const [childState, updateChildState] = useState(
    parentStateData["thisChildData"]
  );

  const updateParentStateDataFromChild = () => {
    return updateParentStateData(
      Object.assign(parentStateData, { thisChildData: childState })
    );
  };

  useEffect(() => updateParentStateDataFromChild(), [childState]);

  return (
    <div className="text-center">
      <h1 className="step-title">Choose Child State</h1>
      <div class="state-size-buttons">
        <Button
          onClick={() => updateChildState(1)}
          active={childState === 1}
          className="mt-3"
          variant="light"
          size="lg"
        >
          1
        </Button>
        <Button
          onClick={() => updateChildState(2)}
          active={childState === 2}
          className="mt-3"
          variant="light"
          size="lg"
        >
          2
        </Button>
        <Button
          onClick={() => updateChildState(3)}
          active={childState === 3}
          className="mt-3"
          variant="light"
          size="lg"
        >
          3
        </Button>
      </div>
      <Button onClick={() => prevStep()} className="mr-2" variant="light">
        Go Back
      </Button>
      <Button disabled={!childState} onClick={() => nextStep()} variant="light">
        Next
      </Button>
    </div>
  );
};

Again, this worked but it's not good (I think) because it duplicates state. The ChildComponent receives the parent state (parentStateData) and an update function for that state (updateParentStateData) but duplicates that state in its own childState and updateChildState - which in turn updates the parent using a useEffect() hook -> when child state updates, update the parent state.

So, I started my refactor, like this:

const ChildComponent = ({
  prevStep,
  nextStep,
  parentStateData,
  updateParentStateData,
}) => {
  const updateState = (data) => {
    return updateParentStateData(
      Object.assign(parentStateData, { thisChildData: data })
    );
  };

  const nextDisabled = !parentStateData["thisChildData"];

  return (
    <div className="text-center">
      <h1 className="step-title">Choose A State</h1>
      <div class="state-size-buttons">
        <Button
          onClick={() => updateState(1)}
          active={parentStateData["thisChildData"] === 1}
          className="mt-3"
          variant="light"
          size="lg"
        >
          1
        </Button>
        <Button
          onClick={() => updateState(2)}
          active={parentStateData["thisChildData"] === 2}
          className="mt-3"
          variant="light"
          size="lg"
        >
          2
        </Button>
        <Button
          onClick={() => updateState(3)}
          active={parentStateData["thisChildData"] === 3}
          className="mt-3"
          variant="light"
          size="lg"
        >
          3
        </Button>
      </div>
      <Button onClick={() => prevStep()} className="mr-2" variant="light">
        Go Back
      </Button>
      <Button
        disabled={nextDisabled}
        onClick={() => nextStep()}
        variant="light"
      >
        Next
      </Button>
    </div>
  );
};

This time as you can see, there is no internal state and the parent state object simply gets updated at the relevant key using Object.assign().

But my problem is these state changes never get reflected in my view. If I do a React component inspector I see the state is changing (although it seems to lag) but the view doesn't change. Most critically, the nextDisabled constant and variants of that (I tried putting that logic directly into the Next button <Button disabled={nextDisabled} onClick={()=>nextStep()} variant="light">Next</Button>) don't ever change. So the parent state gets updated, but !parentStateData['thisChildData'] never changes value or never runs again... so what gets rerendered when the props (via the change in parentStateData) change? Why doesn't the simple validation on the next button work as I expect when I update the parent state? That is, once I select a number using the buttons (no longer 0 from the default) I would expect it to not be disabled (example: !1 is false - so not disabled as its for the disabled prop).

But it never changes!

It stays evaluated to true despite the state updating in the parent and therefore the props changing in the child. So somewhere I have a conceptual error. Any help here would be very much appreciated.

Summer Developer
  • 2,056
  • 7
  • 31
  • 68

1 Answers1

1

React, and most of the tools working with it, are based on the fact that states are immutable. It means that if you want to update a state, you must not change values in the same object, but you need to provide a new object.

So, the following is an error, because it updates the value of the state:

  const updateState = (data) => {
    return updateParentStateData(
      Object.assign(parentStateData, { thisChildData: data })
    );
  };

Instead, you should create a new object, for example:

  const updateState = (data) => {
    return updateParentStateData(
      Object.assign({}, parentStateData, { thisChildData: data })
    );
  };

I think that your first example is working only because it really updates the child property.

Stéphane Veyret
  • 1,791
  • 1
  • 8
  • 15