Reputation: 109
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
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
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