How to use React.Children.toArray to design re-usable components with a simple API
In this demo, I will present a commentary to interface a couple of two or more components to assemble reusable components (fixed functionality defined) with your API interfaced via props.children
The magic of using props.children as an API with a controller component is quite simple and boils down to two steps:
- Configure
props.children
- Pass from component
controller
requiredprops
for your features
Configure props.children
The first is to define if your component accepts, one (and / or) of the react element in children, to accept both:
const children = React.Childen.toArray(props.children)
React.Children.toArray
transforms the variable into an array so that the children.map method is always available. (Which is not the case if you only have one react element in children of your controller component)
- Use
React.Children.toArray
is only of interest if you want to support 1 or more children in your controller. If you still only have one child, in this case you can skip its use.
Inject props
from your controller to children react elements with cloneElement
React.cloneElement
Allows you to clone an already rendered JSX element (Unlike React.createElement which can render a ReactComponent into a JSX element). The 2nd parameter of the function allows to override the props, we can therefore write: :
cloneElement(child, { mine: true })
Done, you know how to use the props.children
as an API interface of your reusable components. il ne vous manque plus qu'à écrire un composant controller pour provisionner des props
à vos children.
Yes but why ?
Let's take the example of a <Carousel />
, if you want to add the following props to each <Slide />:
props.carouselRef
: created within carousel automatically
props.activeIndex
: available using the carousel reference
In bonus, we also want to automatically set the index
on each slide, so we don't need to manually update it on each slide.
props.index
can go up to slides.length - 1
If we want to create a reusable component without exploiting l'API props.children
, we should use another props and should write for example:
function App() {
return (
<Carousel
slides={[Slide1, Slide3, Slide3]}
/>
);
}
// ou
function App() {
return (
<Carousel
slides={[<Slide1 />, <Slide3 />, <Slide3 />]}
/>
);
}
It works very well and for each case we can inject props to each children :
- In the 1st using JSX or more precisely with
createElement
- In the 2nd with
cloneElement
.
So why should you use props.children
? Simply because the interface is more user land friendly (user-land i.e. in your user's code, aka your developer colleague who reuses your component):
<Carousel>
<Slide1 />
<Slide2 />
<Slide3 />
</Carousel>
// et
<Carousel>
<Slide1 /> {/* <= in this case, use React.Children.toArray */}
</Carousel>
Finally, your controller should look like this :
function Carousel(props) {
const carouselRef = useRef<Carousel>();
return (
<ReactCarousel ref={carouselRef}>
{React.Children.toArray(props.children).map((child, index) => cloneElement({
key: child.name
carouselRef,
activeIndex: carouselRef.current.getActiveIndex(),
index,
}))}
</ReactCarousel>
)
}
Some example of feature that can be added :
- A "skip all" button (if many children) ou "skip" (if one child)
- Add analytics on each slide to trigge
pageView
using automatique name that use the index - Event on skip action using the slide name and it's index
When not to use the props.children
API?
- When it is not for API purposes, i.e. your component is not reusable
- When your API is not documented or you do not know how to write or maintain documentation properly so that no one gets it wrong (or when we will have automatic documentation for our components)
- When your API is too complex, ie your component solves too many problems or problems that will probably change, as this may add complexity for those who are not familiar with your components.
Other examples of uses cases
<UserController>
<UserController perPage={25} filters={{ sort: { order: 'ASC', id: 'id' } }}>
<SimpleUserList />
</UserController>
You can now re-use your controller for many kind of UserList
- It is convenient to use a loading indicator when fetching, you will also do it in the controller to normalize its injection through various controllers, for example ...
You can use <UserController />
with <SimpleUserList />
refactored in 2 componants:
- One as a layout controller
- Many for each field that must be shown in the layout
This will give you a lot more of layout re-usability without writing much code, always with an easy to learn interface :
<UserController id={15}>
<SimpleForm>
<TextInput source="id" />
<TextInput source="firstName" />
</SimpleForm>
</UserController>
<UserGrid>
This example illustrates the simplicity offered by this new interface, to offer more grains for example for the display of columns according to the access rights of each: :
<UserController perPage={25} filters={{ sort: { order: 'ASC', id: 'id' } }}>
<UserGrid>
<TextField source="id" />
<TextField source="firstname" />
{isAdmin && (<TextField source="apiKey" />)}
</UserGrid>
</UserController>