Trabajar con React children
Tags: typescript, react, spanish
Published: 2022-02-11 15:23:13

En este artículo voy a tratar de explicar qué pasos se tienen que dar para lograr dos aspectos dentro de un componente de React utilizando para ello TypeScript:

1. Definir el tipo de datos que nos asegure de que el componente en cuestión únicamente va a aceptar como `children` componentes de un determinado tipo.

2. Recorrer todos los hijos de componente haciendo que únicamente se muestren aquellos que son de un determinado tipo y añadiendo además nuevas props al mismo.


## Tipado de `children`

La mejor de forma de entender cómo podemos tipar children dentro de un componentes en React es con un ejemplo. Supongamos que partimos del componente `ComponentA` y que queremos determinar que únicamente va a aceptar como children `ComponentB` ¿cómo lo logramos? Suponiendo que `ComponentA` está definido como un Functional Component nos vamos a encontrar con algo como lo siguiente:

```ts
export const ComponentA: FunctionComponent<T> = ({ 
  children 
}): JSX.Element => ...
```

Está claro que el ejemplo anterior no es correcto para TypeScript pero lo que se trata de recalcar es que `T` es la representación del tipo de datos que recoge las props que recibe nuestro `ComponentA`. ¿Qué quiere esto decir? Pues que podemos definir una interfaz (o tipo) para declarar los tipos de datos que están asociados a nuestras props y utilizarla para declarar el componente. Así pues, si ahora declaramos nuestro componente como sigue:

```ts
export const ComponentA: FunctionComponent<ComponentAProps> = ({ 
  children 
}): JSX.Element => ...
```

Ya solamente nos queda declarar el tipo de datos `ComponentAProps` y más concretamente, definir el tipo de datos que le queremos asignar a `children`. Pero ¿qué tipo de datos es el que le corresponde a esta prop de React? La respuesta es que React nos proporciona el tipo ReactElement para cada uno de los elementos que pueden poblar el Virtual DOM por lo que si nosotros queremos permitir que children sea de estos tipos deberíamos declarar algo como lo que sigue:

```ts
interface ComponentAProps {
  children: ReactElement<S> | Array<ReactElement<S>>
}
```

Es decir, estamos declarando que como `children` vamos a tener tanto un único elemento (que queda representado como `ReactElement<S>`) o varios elementos (de ahí el uso del `Array`, es decir, `Array<ReactElement<S>>`). Pero ¿estamos forzando a que estos elementos sean de un determinado tipo? La respuesta es que no, pero lo que sí que podemos *jugar* es con que tenemos nuevamente un tipo generíco que podemos usar al declararlo (en nuestro ejemplo `S`) por lo que si definimos este tipo generíco como el tipo de datos que definen a las props de los componentes hijos TypeScript ya nos indica que únicamente se permiten esos componentes hijos.

Como la explicación es liosa lo mejor es verlo siguiendo con nuestro ejemplo. Supongamos que el componente hijo que queremos definir (recordemos que es `ComponentB` define en la siguiente interfaz las props que admite):

```ts
interface ComponentBProps {
  // definición de las props
}
```

Lo que ahora podemos hacer al declarar las props de `ComponentA` es hacer uso de esta declaración tal y como sigue:

```ts
interface ComponentAProps {
  children: ReactElement<ComponentBProps> | Array<ReactElement<ComponentBProps>>
}
```

Logrando de esta manera que desde el punto de vista de TypeScript ahora mismo `ComponentA` solamente admita como `children` aquellos elementos que sean un `ReactElement` con las props `ComponentBProps`.

> **Nota:** Aunque desde el punto de vista de TypeScript el tipado es correcto los editores de código como VSCode no van a hacer el chequeo de los tipos mientras estamos desarrollando por lo que hemos logrado tiparlo bien pero no enter reflejo en el código.


## Recorrer `children`

¿Qué pasos tenemos que dar para recorrer todos los `children` que recibe un componente? Pues aquí es donde tenemos que hacer uso del método `map` que nos proporciona el objeto `Children` de React (puedes obtener más información sobre la **API de Alto Nivel de React** [aquí](https://es.reactjs.org/docs/react-api.html)). Es decir, que podemos hacer algo como lo siguiente:

```ts
import { Children } from 'react'

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

Este método acepta dos parámetros siendo el primero de ellos la prop `children` (la que vamos a recorrer) y el segundo una función que se ejecutará sobre cada uno de los elementos que lo conforman. Ahora bien, ¿de qué tipo de datos es cada uno de los elementos? Pues en este caso React nos ofrece el tipo `ReactNode` para representarlo. Esto nos deja con la siguiente declaración:

```ts
import { Children } from 'react'

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

¿Cómo podemos saber el tipo de datos al que pertenece cada uno de los nodos hijos? Pues aquí es donde entra en juego el saber que `ReactNode` tiene un atributo denominado `type` que contiene el tipo de datos al que pertenece el nodo. Por ejemplo, si el nodo en cuestión es del tipo `ComponentB` se puede hacer algo como lo siguiente:

```ts
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) {
	      // .... hacer lo que sea ....
        }
    })}
  </>
)
```

El problema aquí es que TypeScript se va a quejar ya que no puede estar seguro de que el nodo `child` del ejemplo tenga el atributo `type` por lo que es el momento de utilizar una de las funciones de alto que nos proporciona React `isValidElement` la cual retorna `true` en el caso de que el nodo que se está procesando sea un elemento de React y por lo tanto podamos garantizar de que tiene el atributo `type` con los TypeScript nos dejará continuar:

```ts
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) {
	      // .... hacer lo que sea ....
        }
    })}
  </>
)
```

> Si se quiere obtener más información sobre la función `isValidElement` recomendamos [leer la documentación](https://es.reactjs.org/docs/react-api.html#isvalidelement) oficial sobre ella.


## Añadir `props` a los `children`

Como último paso lo que queremos hacer es añadir nuevas props a cada uno de los nodos `children` que cumplen que son del tipo `ComponentB`. En este caso la estrategia que vamos a seguir consiste en hacer uso de la función de Alto Nivel de React denominada `cloneElement` por lo que lo que queremos conseguir es una instancia igual que la que tenemos en el nodo hijo (queremos que se renderice lo mismo), pero sabiendo además que a esta función le podemos pasar un segundo atributo que tendrá un atributo por cada una de las `props` que vamos a inyectar. Así, en el caso de que queremos inyectar la propiedad `injectedProp` escribiríamos algo como lo siguiente:

```ts
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: // lo queramos inyectar (por ejemplo, una función)
					})
        }
    })}
  </>
)
```

Pero ¿cómo reflejarmos y recogemos estas `props` inyectadas en el `ComponentB`? La respuesta es haciendo eso del spread operator de JavaScript para recoger el resto de las `props`, lo que viene a dejarnos algo como lo siguiente:

```ts
export const ComponentB: FunctionComponent<ComponentBProps> = ({ 
  ...props 
}): JSX.Element => ...
```

y de esta manera en el código de `ComponentB` ya podríamos acceder directament a la `injectedProp` como si se tratase de una de las `prop` que han sido declaradas en el componente.

> Si se quiere obtener más información sobre la función `cloneElement` recomendamos [leer la documentación](https://es.reactjs.org/docs/react-api.html#cloneelement) oficial sobre ella.