React avançado — Utilizando “props.children” como função de primeira classe

O coração do React são componentes. Você pode aninhar esses componentes do mesmo modo que você aninha tags HTML, o que torna tudo mais fácil quando escrevemos JSX, afinal, lembra muito o bom e velho HTML.

Quando eu comecei com React, eu pensei “Basta usar props.children e pronto. Eu sei tudo sobre o objeto .children”. Te digo, como eu estava errado! 🙂

Como estamos trabalhando com JavaScript, nós podemos mudar o .children. Nós podemos passar propriedades para ele, decidir se queremos que ele renderize algo ou não, usá-lo do jeito que quisermos. Vamos mergulhar no poder do .children em React!

Abaixo, segue os tópicos que iremos abortar nesse artigo:

Tabela de conteúdo:

  • Elementos filhos
  • Tudo pode ser um elemento filho
  • Função como elemento filho
  • Manipulando .children
  • Usando loops em .children
  • Contando .children
  • Convertendo .children em um array
  • Permitir apenas um elemento filho
  • Editando .children
  • Mudando as propriedades dos elementos filhos
  • Clonando imutavelmente os elementos filhos
  • É isso aí!

Elementos filhos

Vamos dizer que temos um componente , que contém componentes como elementos filhos. Usamos da seguinte maneira:

<Grid>
  <Row />
  <Row />
  <Row />
</Grid>

Demo: http://www.webpackbin.com/Ekg_vyjPz

Esses três componentes Row são passados para o Grid como props.children. Usando um container como expressão (expression container, termo técnico do JSX para os colchetes), componentes pais podem renderizar os elementos filhos:

class Fullstop extends React.Component {
  render() {
    return <h1>Hello world!</h1>
  }
}

Não importa quantos elementos filhos você passe, ele sempre irá renderizar “Hello world!”, e nada mais.

Nota: O h1 no exemplo acima (assim como todas as tags HTML primitivas), está renderizando seu elemento filho, nesse caso, o nó de texto “Hello world!”

 

Tudo pode ser um componente filho

Elementos filhos em React não precisam ser componentes, eles podem ser qualquer coisa! Por exemplo, nós podemos passar para o nosso , do exemplo acima, um nó de texto, e ele irá funcionar perfeitamente:

<Grid>Hello world!</Grid>

Demo: http://www.webpackbin.com/N1FUPocvz

JSX irá, automaticamente, remover todos os espaços em branco, do começo e do fim, e também as linhas em branco. Ele também condensa os espaços em branco do meio do nó de texto para apenas um espaço.

Isso significa, que todos esses exemplos abaixo, são válidos:

<Grid>Hello world!</Grid>
<Grid>
  Hello world!
</Grid>
<Grid>
  Hello
  world!
</Grid>
<Grid>
Hello world!
</Grid>

Você também pode misturar múltiplos tipos de elementos filhos:

<Grid>
  Here is a row:
  <Row />
  Here is another row:
  <Row />
</Grid>

Demo: http://www.webpackbin.com/E1IpLQ3PM

Função como elemento filho:

Nós podemos passar qualquer expressão JavaScript como elemento filho. Isso inclui funções.

Para ilustrar isso, o exemplo à seguir é um componente que executa uma função como elemento filho:

class Executioner extends React.Component {
  render() {
    // Percebe que estamos executando uma função aqui?
    //                        ↓
    return this.props.children()
  }
}

E você usaria esse componente como:

<Executioner>
  {() => <h1>Hello World!</h1>}
</Executioner>

Esse exemplo não foi muito útil, claro, é para lhe apresentar a idéia!

Agora, vamos dar um passo adiante. Imagine que você precisa buscar alguns dados no servidor. Você poderia fazer isso de várias maneiras. Seguindo a idéia de função-como-elemento-filho, a solução seria mais ou menos assim:

<Fetch url="api.myself.com">
  {(result) => <p>{result}</p>}
</Fetch>

Imaginou? Recomendo você tentar implementar a solução nesse link aqui, clica aí!

Caso você queira ver a resposta, nesse link tem a minha solução.

Não se preocupe se você não conseguiu entender tudo de primeira. A idéia aqui é te apresentar esse padrão, para você não ser pego de surpresa quando estiver fazendo aquele freela!

Com elementos, toda a mágica é possível!

Manipulando .children:

Se você olhar na documentação do React, você vai notar um trecho que fala “os elementos filhos são uma estrutura de dados opaca” (”children are an opaque data structure”).

O que eles estão querendo dizer com essa linha, é que os elementos filhos em props.children, podem ser qualquer tipo de objeto JavaScript, assim como arrays, funções ou objetos. Se você pode passar qualquer coisa, você nunca sabe ao certo qual o tipo do elemento filho.

React tem um API que fornece várias funções utilitárias para manipular os elementos filhos de uma maneira bem simples e sem dor de cabeça (você não vai precisar escrever nenhum if).

Essas funções estão disponíveis em React.Children.

Usando loops em .children:

As duas funções utilitárias mais óbvias são React.Children.map e React.Children.forEach. Elas funcionam exatamente como Array.map e Array.forEach, a única diferença é que elas funcionam em funções, objetos ou qualquer outro tipo de elemento passado como elemento filho.

class IgnoreFirstChild extends React.Component {
  render() {
    const children = this.props.children
    return (
      <div>
        {React.Children.map(children, (child, i) => {
          // Ignora o primeiro elemento filho
          if (i < 1) return
          return child
        })}
      </div>
    )
  }
}

O componente , itera todos os elementos filhos, ignorando o primeiro e retornando todos os outros.

<IgnoreFirstChild>
  <h1>First</h1>
  <h1>Second</h1> // <- Só esse será renderizado
</IgnoreFirstChild>

Demo: http://www.webpackbin.com/NyfgFQ2wz

Você pode pensar que poderíamos ter usado this.props.children.map. Mas, o que aconteceria se alguém passe-se uma função como elemento filho? this.props.children seria uma função ao invés de um array, e isso retornaria uma erro! 😱

Com React.Children.map, esse problema está resolvido!

<IgnoreFirstChild>
  {() => <h1>First</h1>} // <- Será ignorado 💪
</IgnoreFirstChild>

Contando .children:

Levando em conta que this.props.children pode ser qualquer tipo, contar quantos elementos filhos um componente tem, começa a ser uma tarefa complicada. Ingenuamente, usar this.props.children.length iria quebrar caso passássemos uma nó de texto ou uma função. Nós teríamos apenas um elemento filho, “Hello World!”, mas o .length iria retornar um valor de 12!

É por isso que nós temos a função utilitária React.Children.count:

class ChildrenCounter extends React.Component {
  render() {
    return <p>React.Children.count(this.props.children)</p>
  }
}

Ele retorna o número de elementos filhos, independente do tipo:

// Renderiza "1"
<ChildrenCounter>
  Second!
</ChildrenCounter>
// Renderiza "2"
<ChildrenCounter>
  <p>First</p>
  <ChildComponent />
</ChildrenCounter>
// Renderiza "3"
<ChildrenCounter>
  {() => <h1>First!</h1>}
  Second!
  <p>Third!</p>
</ChildrenCounter>

Convertendo .children em um array:

Como último recurso, se nenhum dos métodos acima te ajudarem, você pode converter seu elemento filho em um array usando React.Children.toArray. Isso seria útil caso, por exemplo, você precisar ordenar eles:

class Sort extends React.Component {
  render() {
    const children = React.Children.toArray(this.props.children)
    // Ordenando e renderizando os elementos filhos
    return <p>{children.sort().join(' ')}</p>
  }
}

E para usá-lo:

<Sort>
  // Estamos usando “expression containers” para ter certeza
  // que serão passados 3 nós de texto, e não apenas um
  {'bananas'}{'oranges'}{'apples'}
</Sort>

O exemplo acima irá renderizar os nós de texto em ordem:

Demo: http://www.webpackbin.com/NyE2TQhwz

 

Permitir apenas um elemento filho:

Se olharmos nosso primeiro exemplo, , ele está esperando receber apenas um elemento filho, que nesse caso é uma função:

class Executioner extends React.Component {
  render() {
    return this.props.children()
  }
}

Nós poderíamos tentar garantir isso usando propTypes:

Executioner.propTypes = {
  children: React.PropTypes.func.isRequired,
}

Uma mensagem será imprimida no console caso alguém passe um elemento filho diferente, mas, a maioria dos desenvolvedores ignora essas mensagens. Ao invés disso, podemos usar React.Children.only dentro do nosso método render:

class Executioner extends React.Component {
  render() {
    return React.Children.only(this.props.children)()
  }
}

Isso irá garantir que apenas um elemento filho seja executado. Caso passem mais de um elemento filho, um error será retornado, quebrando o app inteiro. Isso é perfeito para evitar que aquele dev preguiçoso bagunce seu componente. 😎

Editando .children

Nós podemos renderizar qualquer tipo de elemento filho, e mesmo assim, ter controle da forma que o elemento pai os renderiza. Para ilustrar isso, vamos dizer que tempos um componente RadioGroup, que contêm um número variado de componentes RadioButton (quer irão renderizar um dentro de uma ).

Os RadioButton não são renderizados pelo RadioGroup, eles são apenas elementos filhos. Isso significa, que, em algum lugar da app nós temos algo como:

render() {
  return(
    <RadioGroup>
      <RadioButton value="first">First</RadioButton>
      <RadioButton value="second">Second</RadioButton>
      <RadioButton value="third">Third</RadioButton>
    </RadioGroup>
  )
}

Tem um bug nesse código. Como os inputs não estão agrupados, o resultado é:

Demo: http://www.webpackbin.com/Vk-Vt_VawM

Para agrupar os inputs, nós precisamos passar um atributo name. Nós podemos resolver isso passando um atributo name para cada RadioButton:

<RadioGroup>
  <RadioButton name="g1" value="first">First</RadioButton>
  <RadioButton name="g1" value="second">Second</RadioButton>
  <RadioButton name="g1" value="third">Third</RadioButton>
</RadioGroup>

Mas, de verdade, isso é:

  • Tedioso de fazer
  • Propenso a erros

Nós temos todo o poder do JavaScript nas mãos. Nós podemos usá-lo para dizer ao RadioGroup que nós queremos que todos os elementos filhos tenham um atributo name, e ele se encarregará de gerá-los automaticamente.

Mudando as propriedades dos elementos filhos:

No nosso RadioGroup, nós vamos adicionar um novo método chamado renderChildren onde nós vamos editar as propriedades dos elementos filhos:

class RadioGroup extends React.Component {
  constructor() {
    super()
    // Realizando o .bind em React ES6
    this.renderChildren = this.renderChildren.bind(this)
  }
renderChildren() {
    // TODO: 
        // Mudar a propriedade “name”
        // de todos os elementos filhos
    return this.props.children
  }
render() {
    return (
      <div className="group">
        {this.renderChildren()}
      </div>
    )
  }
}

Vamos começar iterando sobre os elementos filhos para ter acessado a cada um deles:

renderChildren() {
  return React.Children.map(this.props.children, child => {
    // TODO: 
        // Mudar a propriedade “name”
        // de todos os elementos filhos
    return child
  })
}

A pergunta agora é, como podemos alterar as propriedades dos elementos filhos?

Clonando imutavelmente os elementos filhos:

A última, mas não menos importante, função utilitária de hoje, como o título sugere, é React.cloneElement, para clonar elementos. Nós passamos o elemento alvo como primeiro argumento, e como segundo argumento nós podemos passar um objeto de propriedades que queremos que sejam adicionadas no elemento alvo:

const cloned = React.cloneElement(element, {
  new: 'yes!'
})

Como resultado, o elemento cloned terá uma propriedade props.new com valor ”yes!”.

É exatamente isso que queremos em RadioGroup. Nós clonamos cada elemento filho, alteramos a propriedade name para receber this.props.name do RadioGroup:

renderChildren() {
  return React.Children.map(this.props.children, child => {
    return React.cloneElement(child, {
      name: this.props.name
    })
  })
}

Como última etapa, para agrupar nossos inputs, só precisamos dar um valor único para name em RadioGroup:

<RadioGroup name="g1">
  <RadioButton value="first">First</RadioButton>
  <RadioButton value="second">Second</RadioButton>
  <RadioButton value="third">Third</RadioButton>
</RadioGroup>

Demo: http://www.webpackbin.com/41gz34aDM

E, voilà, funciona! 🎉 Ao invés de manualmente ter que passar um atributo name para cada RadioButton, nós delegamos o attributo name para o RadioGroup e ele se encarrega do resto!

É isso aí!

Elementos filhos em React tornam a escrita mais parecida com linguagem de marcação, como o HTML, e não uma entidade completamente diferente. Usando o poder do JavaScript, e alguns utilitários do React, nós podemos criar API’s declarativas e deixar o nosso dia-a-dia mais fácil!

Gostou do artigo? Deixe seu comentário abaixo!

Deixe um comentário