You've successfully subscribed to Tech news and tutorials for and by DevOps | Kopax
Great! Next, complete checkout for full access to Tech news and tutorials for and by DevOps | Kopax
Welcome back! You've successfully signed in
Success! Your account is fully activated, you now have access to all content.
How to use React.Children.toArray to design re-usable components with a simple API

How to use React.Children.toArray to design re-usable components with a simple API

> 3 min read
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:

  1. Configure props.children
  2. 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>