Dr Spaceman
Dr Spaceman

Reputation: 109

Using React, how can sibling components best share a state? Should there even be a way to traverse DOM tree...?

How can sibling components best share a state in React? Take for example a component that has a series of checkboxes as children: If one is clicked by the user, how can the others be unchecked? (This is just an example to illustrate the problem, ignoring the fact that radio buttons can solve this particular issue.)

<Form>
  <Checkbox />
  <Checkbox checked />
  <Checkbox />
</Form>

function Form() {
  // State and/or context business here maybe?

  return <form>{children}</form>
}

function Checkbox({checked}) {
  const handleClick = (event) => {
    // check this and uncheck all siblings
  }

  return <input type="checkbox" checked onClick={handleClick} />
}

A similar but common use example: Click a box element; Change className to "on"; Change sibling element classNames to "off";

In jQuery, there is a similar way to access siblings through the Tree Traversal API. Something along these lines:

$('input[type="checkbox"]').click(function() {
  $(this).siblings().prop('checked', false);
});

I'm still new to React and not sure if this is a desired functionality.

Upvotes: 0

Views: 290

Answers (2)

Dr Spaceman
Dr Spaceman

Reputation: 109

The solution to having child components share a state is to define the state in the common ancestor, then map the children, assigning an index to each one, using the index as the state value. The following example solution is based on an example from @Yousaf and @Nick Roth's solution to "lift state" to the common ancestor component.

function App() {
  return (
    <Form>
      <Checkbox />
      <Checkbox checked />
      <Checkbox />
    </Form>
  );
}

function Form({ children }) {
  // find the checked component to assign initial state
  let initChecked;
  React.Children.forEach(children, (child, index) => {
      if (child.props.checked) {
          initChecked = index;
      }
  });

  // Lift state to the common ancestor component of the checkboxes
  const [checked, setChecked] = React.useState(initChecked);
  
  // Map children, assigning index and state
  return <form>{React.Children.map(children, (child, index) => {
    if (!React.isValidElement(child)) {
      return child;
    }

    if (isChild(child)) {
      return React.cloneElement(child, {
        index,
        setChecked,
        checked: checked === index,
      });
    }

    return child;
  })}</form>;
}

function Checkbox({ index, checked, setChecked }) {
  return <input type="checkbox" checked onClick={() => setChecked(index)} />
}

function isChild(component) {
  // Some Mechanism to check if the component is a wanted <Checkbox /> component
  return true;
}

Upvotes: 0

Nick Roth
Nick Roth

Reputation: 3077

The general solution for this in React is to "lift state". Essentially if there is shared state between components, find the common ancestor and move the state there. React has some of their own docs on it as well https://reactjs.org/docs/lifting-state-up.html

So in your example, the Form component is where the checked state of the checkboxes should live. Form is the one that would know when other checkboxes get clicked and can coordinate the updates as required.

Upvotes: 2

Related Questions