Skip to content

useEffect for synchronizing state and props #15523

@Yakimych

Description

@Yakimych

Hi. I have a recurring scenario that I’ve been struggling with since the good old days of componentWillReceiveProps, and now I’ve pretty much run into the same issue with hooks, so I was hoping I could get some guidance as to what the idiomatic way of solving this and similar cases in React is.

Problem description - starting point

I have a list of items. Every item has an Edit button next to it. Clicking it opens an “Editor”, where one can change all the fields and either Confirm or Cancel. (Confirming would send an API call to save the data, but this part is not relevant to the problem I am having.) The “parent” component would render the list with the Edit buttons, and have an itemUnderEdit property that would be null from the start. Clicking on “Edit” for a specific item would set the itemUnderEdit to the clicked item.

usecase

Here is the full example with all 3 solutions on CodeSandbox: https://codesandbox.io/s/2oz2nzynpy

Solution 1

Make the “Editor” component stateless and controlled - it takes in change handlers for every field as props with the parent tracking every change. This solution appeals to me, since I like pure stateful components that are a one-to-one mapping of props to HTML - they are simple to reason about etc etc. This kind of goes against the commonly heard “keep your state close to where it is used” advice, which also seems reasonable, since I don’t really need to know in the parent what the user is typing, I am only interested to know when they are done at the end. This stateless solution also introduces a lot of props, since I need one event handler per field (onNameChanged, onDescriptionChanged in the example, but it could as well be 10 fields), which is a lot of props.

Solution 2

Make the “Editor” component stateful and only get an event when editing is done: onConfirm(itemToSave) or onCancel(). This seems like the “React” way and is in line with the advice of keeping state close to where it is used. Since I am only interested to know when the user clicks Confirm, a stateful “blackbox”-component that tracks its own state seems reasonable.

In order to achieve this, however, I need to copy my props to the state, which, according to @gaearon, is a bad idea:

const [name, setName] = useState(props.item.name);
const [description, setDescription] = useState(props.item.description);

Moreover, this solution is buggy from the start, since clicking on Edit for a different item doesn’t “re-sync” the props with the state - it only works if I close the Editor and then reopen it:

stateful_editor1

Which brings us to Solution 3.

Solution 3

This one has been one of my biggest pain-points with stateful components in React (which is why I prefer stateless components with a state container, but those I widely demonized nowadays, so I am yet again trying to understand the idiomatic React way of doing this).
The “old” ways were to sync in componentWillReceiveProps and later with getDerivedStateFromProps. Now I can do this with useEffect, where I specify props.item as the “dependency”, since I want to run it when the item changes.

useEffect(() => {
  if (props.item.name !== name) {
    setName(props.item.name);
  }
  if (props.item.description !== description) {
    setDescription(props.item.description);
  }
}, [props.item]);

This seems to work as expected, but I get the linter warning: React Hook useEffect has missing dependencies: 'description' and 'name'. Either include them or remove the dependency array react-hooks/exhaustive-deps. Obviously if I were to add those to the dependency list, I wouldn’t be able to change anything in the inputs, so how come I get this warning?

Summary

This is a question in two parts: first one about an idiomatic solution in React, as well as feedback to the React team: this scenario is simple and common, but it’s difficult to know how to implement correctly and safely in a consistent way.

Lifting state up and making the problematic component stateless is good advice that solves the problem, but every time it seems like a “temporary” solution. It also leads to painful refactoring every time something has to be moved around the component tree, so relying on it in the long run is extremely brittle.

The second part of the question is whether the solution with useEffect is viable at all, and in this case - why do I get the linter warning? Clearly I want to run it only when a certain prop changes. Is there an edge-case where this would result in an unexpected bug?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions