'How do you use React.cloneElement through Fragments?
I'm using React.cloneElement to let a parent component control some of the props of it's children. A lot of the complexity of the ReactNode data structure is handled by the React helper methods React.Children.map and React.Children.toArray, like how they flatten Arrays for example, but they do NOT flatten Fragments. In my use case I would like to clone through the fragments so the props of any children in the Fragment get set as expected. My question is, is there a "built-in" way to do this, is it a reasonable thing to expect, and is my solution I'll show at the end OK if there isn't a better way?
For example:
<Parent>
<Child text="A" />
{ true && <Child text="B" /> }
{ false && <Child text="C" /> }
{ [
<Child text="D" />,
<Child text="E" />
]}
<>
<Child text="F" />
<Child text="G" />
</>
</Parent>
Here's how the helper methods behave in these situations:
React.Children.Map:
{ false && <Child text="C" /> }is converted tonull- The array with D and E is flattened
- The fragment with F and G in it is NOT flattened, you are just handed the Fragment element.
React.Children.toArray:
{ false && <Child text="C" /> }is removed (not included in the returned array)- The array with D and E is flattened
- The fragment with F and G in it is NOT flattened, the Fragment element is included in the array.
This means when I do something simple like:
return (
<div>
{React.Children.map(props.children, c => {
if (!React.isValidElement(c)) return c;
else return React.cloneElement(c, {...c.props, isWhatever: true})
})}
</div>
);
The child elements inside of the Fragment will not get isWhatever set to true because the Fragment itself is returned and cloned and the child elements are not.
The solution I have come up with is to do something to recursively flatten Fragments like this:
function cloneThroughFragments(children: React.ReactNode) : React.ReactNode {
return React.Children.map(children, (c) => {
if (React.isValidElement(c) && (c as any).type === Symbol.for("react.fragment")) {
return cloneThroughFragments(c.props.children);
}
else {
if (!React.isValidElement(c)) return c;
return React.cloneElement(c, {...c.props, isWhatever: true});
}
});
}
Is there a better way to do this? Or a reason why I shouldn't do this? If not, is this the best way to match the Fragment?
NOTE: I learned the hard way that it is really important that the cloneElement calls are INSIDE of the React.Children.map callback. One version I tried build a flattened array, then looped over the flattened array and called cloneElement, but that resulted in key warnings from React! And you can't easily use toArray to do this because it will end up generating duplicate keys for the Fragment children. There are a lot of surprises and gotchas and "internal" React knowledge needed at this level, which is really why I'm asking this question.
Solution 1:[1]
Your solution is good, but you don't have to use magic Symbols, when you can directly compare the type (since 0.13):
function cloneThroughFragments(children: React.ReactNode) : React.ReactNode {
return React.Children.map(children, (c) => {
if (React.isValidElement(c)) {
if (c.type === React.Fragment) { // just compare to `React.Fragment`
return cloneThroughFragments(c.props.children);
}
return React.cloneElement(c, {...c.props, isWhatever: true});
}
return c;
});
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | smac89 |
