Kep
Kep

Reputation: 5857

Nested Components not updating their state

I'm having some trouble updating nested components in my tree structure. I have created the following minimal example to illustrate the problem: Codesandbox.io

For completeness sake, this is the component that's being nested:

class Node extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      selected: props.selected
    };
    this.toggleSelected = this.toggleSelected.bind(this);
  }

  toggleSelected() {
    this.setState({
      selected: !this.state.selected
    });
  }

  render() {
    return (
      <>
        <div onClick={this.toggleSelected}>
          Selected: {this.state.selected ? "T" : "F"}, Depth: {this.props.depth}
        </div>

        {this.props.depth < 5 && (
          <Node selected={this.state.selected} depth={this.props.depth + 1} />
        )}
      </>
    );
  }
}

Nodes in the tree should be selectable on click and I'd like to toggle the selected state in all children (recursively) aswell. I thought I could do this by passing this.state.selected via props to the child/children, unfortunately this doesn't seem to work.

The children get re-rendered, however using their old state (understandibly, as they're not being re-initialized via the constructor). What would be the correct way to handle this?

I've tried passing the key prop to the nodes aswell to help react distinguish the elements, to no avail.

Edit: Here are a few examples of desired behaviour:

Consider this tree:

[ ] Foo
    [ ] Foo A
        [ ] Foo A1
        [ ] Foo A2
    [ ] Foo B
        [ ] Foo B1
        [ ] Foo B2

Expected result when checking "Foo"-Node:

[x] Foo
    [x] Foo A
        [x] Foo A1
        [x] Foo A2
    [x] Foo B
        [x] Foo B1
        [x] Foo B2

Expected result when checking "Foo A"-Node:

[ ] Foo
    [x] Foo A
        [x] Foo A1
        [x] Foo A2
    [ ] Foo B
        [ ] Foo B1
        [ ] Foo B2

Any tips / hints in the right direction are appreciated.

Upvotes: 7

Views: 1435

Answers (2)

Emile Bergeron
Emile Bergeron

Reputation: 17430

Disclaimer: Instead of fixing the code in OP's question, I'm demonstrating how to render a tree and manage its state in a React app.


It would be way easier to create a data-driven tree of stateless Node components and leave the state management to the root component.

Each node receives the selectedId and compares it with their own id to know if they (and their children) should be rendered as active.

They also receive a setSelected callback to notify the parent that they were clicked on.

Being driven by the data (their unique ID, the selected state, etc) from the parent, it becomes more generic and leaves a lot of place to augment the tree with new features. It's also decoupled, easier to test, etc.

// Stateless node
const Node = ({ depth, setSelected, selected, id, nodes, selectedId }) => {
  const isSelected = selected || selectedId === id;
  return (
    <React.Fragment>
      {id && (
        <div style={{ marginLeft: `${depth * 15}px` }}>
          <input
            type="checkbox"
            onChange={() => setSelected(id)}
            checked={isSelected}
          />
          {id}
        </div>
      )}

      {depth < 5 &&
        nodes.map(nodeProps => (
          <Node
            key={nodeProps.id}
            {...nodeProps}
            selected={isSelected}
            selectedId={selectedId}
            setSelected={setSelected}
            depth={depth + 1}
          />
        ))}
    </React.Fragment>
  );
};

Node.defaultProps = {
  selected: false,
  nodes: [],
  depth: 0
};

// Parent keeps the selected Id
class App extends React.Component {
  state = {
    selected: null
  };

  setSelected = id => {
    this.setState(({ selected }) => ({
      selected: selected !== id ? id : null
    }));
  };

  render() {
    return (
      <div className="App">
        <Node
          nodes={this.props.tree}
          selectedId={this.state.selected}
          setSelected={this.setSelected}
        />
      </div>
    );
  }
}

const treeData = [
  {
    id: "Foo",
    nodes: [
      {
        id: "Foo A",
        nodes: [{ id: "Foo A1" }, { id: "Foo A2" }]
      },
      {
        id: "Foo B",
        nodes: [{ id: "Foo B1" }, { id: "Foo B2" }]
      }
    ]
  }
];

ReactDOM.render(
  <App tree={treeData} />,
  document.getElementById("root")
);
input {
  cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>

This is the same concept as the duplicate target: How to update parent's state in React? but with a personalized example to help understand the relationship between the current use-case and the concept behind.

Upvotes: 0

sudheer singh
sudheer singh

Reputation: 922

You should use getDerivedStateFromProps like this:

    constructor(props) {
       super(props);
       this.state = {
         selected: props.selected,
         propsSelected: props.selected
       };
       this.toggleSelected = this.toggleSelected.bind(this);
    }



    static getDerivedStateFromProps(props, state) {
        if (props.selected !== state.propsSelected)
          return {
            selected: props.selected,
            propsSelected: props.selected
          };
      }

We always store the prevProp in state. Whenever we encounter a change in the props that are stored in the state and the props coming from the parent, we update the state part (selected) being controlled both from parent and the component itself and we also preserve the props at that point, in state for future diffing.

Usually a component which can be controlled from both the parent and itself will involve a logic of this sort. An input component found in most react component libraries is an ideal example.

Upvotes: 3

Related Questions