DEV Community

Cover image for Working with React children
DevJoseManuel
DevJoseManuel

Posted on • Edited on

Working with React children

In this article I will try to explain what steps have to be taken to achieve two aspects within a React component using TypeScript:

  1. Define the data type that ensures that the component in question will only accept components of a certain type as children.

  2. Traverse all component children making only those that are of a certain type be shown and adding new props to it.

Type of children

The best way to understand how we can type children inside a component in React is with an example. Let's suppose that we start with the component ComponentA and we want to determine that it is only going to accept ComponentB as children, how can we do it? Supposing that ComponentA is defined as a Functional Component we are going to find something like the following:

export const ComponentA: FunctionComponent<T> = ({ 
  children 
}): JSX.Element => ...
Enter fullscreen mode Exit fullscreen mode

It is clear that the previous example is not correct for TypeScript but what we are trying to emphasize is that T is the representation of the data type that collects the props that our ComponentA receives. What does this mean? Well, we can define an interface (or type) to declare the data types that are associated to our props and use it to declare the component. So, if we now declare our component as follows:

export const ComponentA: FunctionComponent<ComponentAProps> = ({ 
  children 
}): JSX.Element => ...
Enter fullscreen mode Exit fullscreen mode

Now we only have to declare the ComponentAProps data type and more specifically, define the data type that we want to assign to children. But what data type is the one that corresponds to this React prop? The answer is that React provides us with the ReactElement type for each of the elements that can populate the Virtual DOM so if we want to allow children to be of these types we should declare something like the following:

interface ComponentAProps {
  children: ReactElement<S> | Array<ReactElement<S>>
}
Enter fullscreen mode Exit fullscreen mode

That is, we are declaring that as children we will have either a single element (which is represented as ReactElement<S>) or several elements (hence the use of Array, i.e. Array<ReactElement<S>>). But are we forcing these elements to be of a certain type? The answer is no, but what we can play with is that we again have a generic type that we can use when declaring it (in our example S) so if we define this generic type as the data type that defines the props of the child components TypeScript already tells us that only those child components are allowed.

As the explanation is complicated it is better to see it following with our example. Let's suppose that the child component that we want to define (let's remember that it is ComponentB defines in the following interface the props that it supports):

interface ComponentBProps {
  // props definition
}
Enter fullscreen mode Exit fullscreen mode

What we can now do when declaring the ComponentA props is to make use of this declaration as follows:

interface ComponentAProps {
  children: ReactElement<ComponentBProps> | Array<ReactElement<ComponentBProps>>
}
Enter fullscreen mode Exit fullscreen mode

Achieving in this way that from the point of view of TypeScript right now ComponentA only admits as children those elements that are a ReactElement with the ComponentBProps props.

Note: Although from TypeScript's point of view the typing is correct the code editors like VSCode will not do the type checking while we are developing so we have managed to type it right but it is not reflected in the code.

Visit children

What steps do we have to take to go through all the children that a component receives? Well, this is where we have to make use of the map method provided by the React Children object (you can get more information about the React High-Level API here). That is, we can do something like the following:

import { Children } from 'react'

export const ComponentA: FunctionComponent<ComponentAProps> = ({ 
  children 
}): JSX.Element => (
    <>
        { Children.map(....)}
    </>
)
Enter fullscreen mode Exit fullscreen mode

This method accepts two parameters, the first one being the children prop (the one we are going to traverse) and the second one a function that will be executed on each of the elements that conform it. Now, what type of data is each of the elements? Well, in this case React offers us the ReactNode type to represent it. This leaves us with the following declaration:

import { Children } from 'react'

export const ComponentA: FunctionComponent<ComponentAProps> = ({ 
  children 
}): JSX.Element => (
    <>
        { Children.map(children, (child: ReactNode) => {})}
    </>
)
Enter fullscreen mode Exit fullscreen mode

How can we know the data type to which each of the child nodes belongs? Well, this is where it comes into play knowing that ReactNode has an attribute called type that contains the type of data to which the node belongs. For example, if the node in question is of type ComponentB you can do something like the following:

import { Children } from 'react'
import { ComponentB } from './ComponentB'

export const ComponentA: FunctionComponent<ComponentAProps> = ({ 
  children 
}): JSX.Element => (
  <>
    { Children.map(children, (child: ReactNode) => {
        if (child.type === ComponentB) {
        // .... do stuff ....
        }
    })}
  </>
)
Enter fullscreen mode Exit fullscreen mode

The problem here is that TypeScript is going to complain as it cannot be sure that the child node in the example has the type attribute so it is time to use one of the stop functions provided by React isValidElement which returns true in case the node being processed is a React element and therefore we can guarantee that it has the type attribute with the TypeScript will let us continue:

import { Children } from 'react'
import { ComponentB } from './ComponentB'

export const ComponentA: FunctionComponent<ComponentAProps> = ({ 
  children 
}): JSX.Element => (
  <>
    { Children.map(children, (child: ReactNode) => {
        if (isValidElement(child) && child.type === ComponentB) {
        // .... do stuff ....
        }
    })}
  </>
)
Enter fullscreen mode Exit fullscreen mode

If you want to learn more about the isValidElement function we recommend read the official documentation about it.

Add props to the children

As a last step what we want to do is to add new props to each of the children nodes that meet that are of type ComponentB. In this case the strategy that we are going to follow consists of making use of the function of High Level of React called cloneElement so what we want to obtain is an instance equal to the one that we have in the child node (we want that the same thing is rendered), but knowing in addition that to this function we can pass a second attribute that will have an attribute for each one of the props that we are going to inject. Thus, in the case that we want to inject the injectedProp property we would write something like the following:

import { Children } from 'react'
import { ComponentB } from './ComponentB'

export const ComponentA: FunctionComponent<ComponentAProps> = ({ 
  children 
}): JSX.Element => (
  <>
    { Children.map(children, (child: ReactNode) => {
        if (isValidElement(child) && child.type === ComponentB) {
    return cloneElement(child, { 
                injectedProp: // what we want to inject it (for example, a function)
          })
        }
    })}
  </>
)
Enter fullscreen mode Exit fullscreen mode

But how do we reflect and collect these props injected into the ComponentB? The answer is by doing that JavaScript spread operator thing to pick up the rest of the props, which leaves us with something like the following:

export const ComponentB: FunctionComponent<ComponentBProps> = ({ 
  ...props 
}): JSX.Element => ...
Enter fullscreen mode Exit fullscreen mode

and this way in the code of ComponentB we could access directly to the injectedProp as if it was one of the prop that have been declared in the component.

If you want to learn more about the cloneElement function we recommend read the official documentation about it.

Top comments (0)