This is similar to wrap children for react component which has children restricted to certain types, but I'm not using Flow or TypeScript. I maintain a component library and have seen the following pattern come up a few times.
There is a set of composable components where the parent component expects specific component types for children:
<Tabs>
<Tab />
<Tab />
<Tab />
</Tabs>
or
<Stepper>
<Step status="completed" />
<Step status="started" />
<Step status="notstarted" />
</Stepper>
The public APIs for the parent components say something like "children must be zero or more <X> elements" because we need to get in and either read props from these children or rely on their presence for other reasons.
However, we find authors often doing things like the following for valid reasons:
function MyTabChildren() {
// Do stuff
return (
<>
<Tab />
<Tab />
<Tab />
</>
);
}
<Tabs>
<MyTabChildren />
</Tabs>
Or, more commonly, we also use styled-components, so we could have something like the following:
import styled from "styled-components";
const StyledTab = styled(Tab)`
// make it nice
`;
<Tabs>
<StyledTab />
<StyledTab />
<StyledTab />
</Tabs>
As a side note, if we were doing something like the following, an HTML validity check would pass fine:
function MyListItems() {
// Do stuff
return (
<>
<li>One</li>
<li>Two</li>
<li>Three</li>
</>
);
}
<ul>
<MyListItems />
</ul>
So I'm not really asking about these specific implementations or how to get styled-components to be invisible.
We've found ourselves doing things like the following:
SCENARIO 1
function Tabs({ children }) {
const filteredChildren = React.Children.toArray(children).filter((child) => {
return child.type === Tab;
});
// Do stuff...
}
SCENARIO 2
function Stepper({ children }) {
const statuses = React.Children.map(
children,
(child) => child.props.status
);
// Do stuff...
}
But these become problematic if the children are wrapped in any way—even though the wrapper may ultimately only be rendering the expected UI. There is also the case where there may be a few levels of wrapping before the needed element is actually rendered.
Is there a good pattern for having expected children types while still allowing for "invisible" wrapping to occur? Or is doing this type of thing an anti-pattern?
(For Scenario 2, I thought about the parent providing a context that includes a state "setter" and the children could consume the context and call the setter, but this seems like too many re-renders.)