Padrões em React: Criando Componentes

Em detalhes, vários padrões e técnicas para componentes React!

Após algum tempo utilizando certa tecnologia ou biblioteca, você começa a notar padrões criados pela comunidade em volta dessa ferramenta, ou, oficialmente recomendado pelos autores.

React tem mostrado que teremos um futuro excitante! Não só trouxe várias idéias para web, mas também, nos fez repensar várias outras. Inclusive, em áreas como desenvolvimento de aplicativos móveis, realidade virtual e muitas outras.

Com esse novo e amplo campo, vários padrões foram pensados, criados, repensados e recriados.

Hoje, iremos listar boa parte dos padrões criados ao escrever código React. A lista parece longa, mas os exemplos são curtos e diretos. O café tá quente? Relaxa que não vai esfriar!

Tabela de conteúdo

  1. Componentes sem estado
  2. Utilizando JSX spread em atributos
  3. Realizando o destructuring de argumentos
  4. Renderização condicional
  5. Tipos de children
  6. Utilizando array como children
  7. Utilizando function como children
  8. Utilizando o atributo render callback
  9. Repassando children diretamente
  10. Componentes de abstração
  11. Componentes de abstração com estilos parciais
  12. Utilizando reducers ao registrar eventos
  13. Componentes de abstração de layout
  14. Container Components
  15. High-order Components
  16. Movendo estados para um componente superior
  17. Componentes controlados

1. Componentes sem estado

Componentes sem estado são uma brilhante idéia para definir componentes reutilizáveis. Eles não guardam estado, são apenas simples funções.

const Greeting = () => <div>Hi there!</div>

Eles recebem props e context :

const Greeting = (props, context) =>
<div style={{color: context.color}}>Oi {props.name}!</div>

Eles podem definir variáveis locais:

const Greeting = (props, context) => {
const style = {
fontWeight: "bold",
color: context.color,
}

return <div style={style}>{props.name}</div>
}

Mas você pode ter o mesmo resultado extraindo a lógica de dentro dele:

const getStyle = context => ({
fontWeight: "bold",
color: context.color,
})

const Greeting = (props, context) =>
<div style={getStyle(context)}>{props.name}</div>

Você pode definir defaultProps , propTypes e contextType :

Greeting.propTypes = {
name: PropTypes.string.isRequired
}
Greeting.defaultProps = {
name: "Leitor"
}
Greeting.contextTypes = {
color: PropTypes.string
}

2. Utilizando JSX spread em atributos

A técnica de spread attributes, é uma característica do JSX (sim, é a mesma API do ES6 Spread). É apenas açúcar sintático para passar todas as propriedades de um objeto como atributos JSX.

Esses exemplos são equivalentes:

// props escritas como atributos
<main className="main" role="main">{children}</main>

// realizando o "spread" das props do objeto
<main {...{className: "main", role: "main", children}} />

Você pode utilizar essa técnica para passar props para componentes filhos:

const FancyDiv = props =>
<div className="fancy" {...props} />

Agora, FancyDiv podemos adicionar atributos que esse componente não conhece:

<FancyDiv data-id="my-fancy-div">So Fancy</FancyDiv>

// saída: <div className="fancy" data-id="my-fancy-div">So Fancy</div>

Mas lembre-se, a ordem é importante! Se você tem props.className definida, ela será sobrescrita pela prop passada:

<FancyDiv className="my-fancy-div" />

// saída: <div className="my-fancy-div"></div>

Podemos fazer com que o className da FancyDiv sempre ganhe, só precisamos declarar a propriedade depois do spread:

// agora `className` não afetará `className` interno
const FancyDiv = props =>
<div {...props} className="fancy" />

Uma boa opção, é sempre unir essas classes:

const FancyDiv = ({ className, ...props }) =>
<div
className={["fancy", className].join(' ')}
{...props}
/>

3. Realizando o destructuring de argumentos

destructuring assignment, é uma característica do ES2015. Funciona bem com props em funções sem estado.

Esses são equivalentes:

const Greeting = props => <div>Hi {props.name}!</div>

const Greeting = ({ name }) => <div>Hi {name}!</div>

A sintaxe de rest parameter, permite coletar as propriedades restantes em um novo objeto:

const Greeting = ({ name, ...props }) =>
<div>Hi {name}!</div>

Podemos utilizar a mesma sintaxe para passar props para componentes filhos:

const Greeting = ({ name, ...props }) =>
<div {...props}>Hi {name}!</div>

Evite repassar props não válidas do DOM. Utilizando destructuring, deixa essa tarefa mais fácil, porque você pode criar um novo objeto sem as props específicas do componente.

4. Renderização condicional

Você não pode usar condições if / else dentro da renderização de um componente. O operador condicional ternário pode nos ajudar.

Simulando if

{condition && <span>Renderiza quando for `truthy`</span> }

Simulando unless

{condition || <span>Renderiza quando for `falsey`</span> }

Simulando if-else (única linha)

{condition
? <span>Rendered when `truthy`</span>
: <span>Rendered when `falsey`</span>
}

Simulando if-else (múltiplas linhas)

{condition ? (
<span>
Rendered when `truthy`
</span>
) : (
<span>
Rendered when `falsey`
</span>
)}

Você também pode criar alguns componentes para te ajudar nessa tarefa, tais como Either e If :

Simulando Either

const Either = props => props.when ? props.right : props.left;<Either
when={condition}
right={<span>Renderiza quando for `truthy`</span>}
left={<span>Renderiza quando for `falsey`</span>}
/>

Simulando If

const If = props => props.check ? props.children : null;<If check={condition}>
<span>Renderiza quando for `truthy`</span>
</If>

5. Tipos de children

React pode renderizar children de várias formas, devido a sua estrutura de dados opaca, na maioria dos casos ou é um array ou uma string .

Usando string

<div>
Hello World!
</div>

Usando array

<div>
{["Hello ", <span>World</span>, "!"]}
</div>

Também podemos usar funções (mais sobre nas próximos sessões):

<div>
{() => { return "hello world!"}()}
</div>

E a partir do React 16, podemos renderizar um array de componentes:

render() {
// Sem a necessidade de um elemento extra
return [
// Não se esqueça das `keys` :)
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>,
];
}

Você também pode utilizar o padrão de componentes auxiliares:

const Aux = (props) => {
return props.children;
};

const Root = () => {
return <Aux>
<p>Hello, World!</p>
<p>I am a demo for aux.</p>
</Aux>;
};

6. Utilizando array como children

Fornecer uma array como children é muito comum. Podemos usar map() para renderizar um array de elementos React.

<ul>
{["first", "second"].map((item) => (
// Não se esqueça, `key` deve ser única
<li key={item}>{item}</li>
))}
</ul>

É equivalente ao:

<ul>
{[
<li key="first">first</li>,
<li key="second">second</li>,
]}
</ul>

Esse padrão pode ser combinado com a desestruturação de atributos JSX e outros componentes:

<ul>
{arrayOfMessageObjects.map(({ id, ...message }) =>
<Message key={id} {...message} />
)}
</ul>

7. Utilizando function como children

Usar uma função como children não é inerentemente útil.

<div>{() => { return "hello world!"}()}</div>

No entanto, ele pode ser usado na criação de componentes com sérios poderes. Esta técnica é comumente chamada de render callback.

Esta é uma técnica poderosa usada por bibliotecas como o ReactMotion. Quando aplicado, a lógica de renderização pode ser mantida no componente atual, ao invés ser delegada para a biblioteca.

Leia abaixo para saber mais.

8. Utilizando o atributo render callback

Aqui está um componente que usa render callback. Não é útil, mas é uma ilustração fácil de começar.

const Width = ({ children }) => children(500)

O componente chama children como uma função, com algum número de argumentos. Aqui, é o número 500.

Para usar este componente, invocamos uma função como children .

<Width>
{width => <div>window is {width}</div>}
</Width>

Teremos:

<div>window is 500</div>

Com isso, podemos começar a tomar decisões:

<Width>
{width =>
width > 600
? <div>min-width requirement met!</div>
: null
}
</Width>

Se planejarmos usar esta condição em vários lugares, podemos definir outros componentes para encapsular a lógica reutilizada.

const MinWidth = ({ width: minWidth, children }) =>
<Width>
{width =>
width > minWidth
? children
: null
}
</Width>

Obviamente, um componente de largura estática não é útil, mas aquele que observa o navegador é. Aqui está uma implementação de exemplo.

class WindowWidth extends React.Component {
constructor() {
super()
this.state = { width: 0 }
}

componentDidMount() {
this.setState(
{width: window.innerWidth},
window.addEventListener(
"resize",
({ target }) =>
this.setState({width: target.innerWidth})
)
)
}

render() {
return this.props.children(this.state.width)
}
}

Muitos desenvolvedores preferem High-order Components para esse tipo de funcionalidade. É uma questão de preferência.

9. Repassando children diretamente

Você pode criar um componente projetado para aplicar o context e renderizar children .

class SomeContextProvider extends React.Component {
getChildContext() {
return {some: "context"}
}

render() {
// qual melhor maneira de retornar `children`?
}
}

Você está diante de uma decisão. Envolva children em um div qualquer ou devolva children diretamente. A primeira opção oferece uma marcação extra (que pode quebrar alguns estilos). O segundo resultará em erros inúteis.

// opção 1: div extra
return <div>{children}</div>

// opção 2: erros inúteis
return children

É melhor tratar as children como um tipo de dados opaco. React fornece React.Children para lidar com isso:

return React.Children.only(this.props.children)

10. Componentes de abstração

Botões estão em todos os lugares nas aplicações web. E cada um deles deve ter o atributo type definido como button.

<button type="button">

Escrever esse atributo várias vezes é propenso a erros. Podemos escrever um componente de abstração para guardar props comuns:

const Button = props =>
<button type="button" {...props}>

Podemos esse componente no lugar do botão e garantir que o atributo type seja usado consistentemente em todos os lugares.

<Button />
// <button type="button"><button>

<Button className="CTA">Send Money</Button>
// <button type="button" class="CTA">Send Money</button>

11. Componentes de abstração com estilos parciais

Esse padrão é baseado nos componentes de abstração, porém, eles também contém estilos.

Digamos que temos um botão e ele usa classes denominar um botão “primário”.

<button type="button" className="btn btn-primary">

Podemos gerar alguns componentes de um único propósito:

import classnames from 'classnames'

const PrimaryBtn = props =>
<Btn {...props} primary />

const Btn = ({ className, primary, ...props }) =>
<button
type="button"
className={classnames(
"btn",
primary && "btn-primary",
className
)}
{...props}
/>

Isso pode ajudar a visualizar a idéia:

PrimaryBtn()
↳ Btn({primary: true})
↳ Button({className: "btn btn-primary"}, type: "button"})
↳ '<button type="button" class="btn btn-primary"></button>'

Usando esses componentes, tudo isso resulta na mesma saída:

<PrimaryBtn />
<Btn primary />
<button type="button" className="btn btn-primary" />

Isso pode ser uma grande vantagem para a manutenção de estilos. Isolamos todas as preocupações de estilo em um único componente.

12. Utilizando reducers ao registrar eventos

Ao manipularmos eventos, é comum adotar a convenção de nomeação handle{NomeDoEvento} .

handleClick(e) { /* faz algo */ }

Para componentes que manipulam vários tipos de eventos, esses nomes de funções podem ser repetitivos. Os próprios nomes podem não fornecer muito valor, pois eles simplesmente são delegam a lógica para outras ações/funções.

handleClick() { require("./actions/doStuff")(/* faz algo */) }
handleMouseEnter() { this.setState({ hovered: true }) }
handleMouseLeave() { this.setState({ hovered: false }) }

Um padrão comum é um único manipulador de eventos para o seu componente e identificar as ações a partir do event.type.

handleEvent({type}) {
switch(type) {
case "click":
return require("./actions/doStuff")(/* faz algo */)
case "mouseenter":
return this.setState({ hovered: true })
case "mouseleave":
return this.setState({ hovered: false })
default:
return console.warn(`Nenhuma ação para "${type}"`)
}
}

Você pode mover esse switch fora do componente, deixando testes ainda mais simples.

Alternativamente, para componentes simples, você pode chamar ações/funções importadas diretamente dos componentes:

<div
onClick={() => someImportedAction({ action: "DO_STUFF" })}
...outras props
/>

Você pode encontrar por aí que funções anônimas na função render podem causar problemas de performance. Não se preocupe com otimizações de desempenho até ter problemas com. Sério, não.

13. Componentes de abstração de layout

Os componentes de layout resultam em alguma forma de elemento DOM estático. Pode não ser necessário atualizar eles com freqüência.

Considere um componente que renderiza dois elementos filhos lado a lado.

<HorizontalSplit
leftSide={<SomeSmartComponent />}
rightSide={<AnotherSmartComponent />}
/>

Podemos otimizar este componente de várias formas.

Enquanto o HorizontalSplit será o pai de ambos os componentes, ele não terá nenhuma influência em ambos. Podemos dizer para ele nunca se atualizar, sem interromper o ciclo de vida dos componentes dentro.

class HorizontalSplit extends React.Component {
shouldComponentUpdate() {
return false
}

render() {
<FlexContainer>
<div>{this.props.leftSide}</div>
<div>{this.props.rightSide}</div>
</FlexContainer>
}
}

14. Container Componentes

Container Components são componentes que guardam estado e repassam para seus componentes filhos. Um exemplo disso, seria um componente que busca dados em algum serviço e atualiza seus componentes filhos.

Vamos imaginar um componente sem estado como CommentList :

const CommentList = ({ comments }) =>
<ul>
{comments.map(comment =>
<li>{comment.body}-{comment.author}</li>
)}
</ul>

Podemos criar um novo componente responsável pela busca de dados e renderização dos do CommentList :

class CommentListContainer extends React.Component {
constructor() {
super()
this.state = { comments: [] }
}

componentDidMount() {
$.ajax({
url: "/my-comments.json",
dataType: 'json',
success: comments =>
this.setState({comments: comments});
})
}

render() {
return <CommentList comments={this.state.comments} />
}
}

Podemos escrever container components para diferentes contextos da nossa aplicação.

15. High-order Components

Esse padrão se origina do conceito de high-order functions, onde uma função recebe e retorna uma função. Então, o que é um high-order component?

Se você já utilizou container components, high-order component envolvem um desses componentes em uma função e retornam um novo componente (como uma factory function).

Vamos começar com um componente sem estado Greeting :

const Greeting = ({ name }) => {
if (!name) { return <div>Connecting...</div> }

return <div>Hi {name}!</div>
}

Se ele receber props.name ele irá renderizar esse dado, se não, irá renderizar Connecting... . Agora, vamos ver nosso high-order component:

const Connect = ComposedComponent =>
class extends React.Component {
constructor() {
super()
this.state = { name: "" }
}
componentDidMount() {
// poderíamos buscar dados ou conectar a uma `store`
this.setState({ name: "Leitor" })
}
render() {
return (
<ComposedComponent
{...this.props}
name={this.state.name}
/>
)
}
}

É apenas uma função que retorna o componente e processa o componente que passamos como um argumento.

Última etapa, precisamos envolver nosso nosso componente com nosso high-order componente.

const ConnectedMyComponent = Connect(Greeting)

Este é um padrão poderoso para fornecer busca de dados a qualquer número de componentes sem estado.

16. Movendo estados para um componente superior

Componentes sem estado, não guardam nenhum estado (como o próprio nome implica).

Eventos são mudanças em algum estado. Esses dados precisam ser passados para um componente que guarda estado, como container components.

Isso é a prática de “mover estado para um componente superior”. Você realiza isso ao passar uma função como callback de um container componente para um componente sem estado.

class NameContainer extends React.Component {
render() {
return <Name onChange={newName => alert(newName)} />
}
}

const Name = ({ onChange }) =>
<input onChange={e => onChange(e.target.value)} />

Name irá receber onChange como callback do NameContainer e chamar esse evento.

O alert acima, é um exemplo bobo, pois não está mudando nenhum estado. Vamos alterar nosso exemplo:

class NameContainer extends React.Component {
constructor() {
super()
this.state = {name: ""}
}

render() {
return <Name onChange={newName => this.setState({name: newName})} />
}
}

Nosso estado está localizado no NameContainer e ao executar o callback o estado será alterado. Isso define um limite claro e agradável e maximiza a reutilização da função sem estado.

Este padrão não se limita a componentes sem estado. Uma vez que o componente sem estado não tem eventos do ciclo de vida, você usará esse padrão com classes de componentes também.

Veremos mais sobre sobre isso abaixo, em Componentes controlados

17. Componentes controlados

É difícil falar sobre componentes controlados sem mostrar algum exemplo. Vamos começar com um campo de texto descontrolado (ou "normal"):

<input type="text" />

Quando você usa esse componente no navegador, você vê suas mudanças. Isto é normal.

Um componente controlado não permite essas mutações do DOM. Você define todos os valores de entrada em seu componente e não muda ele através do DOM.

<input type="text" value="This won't change. Try it." />

Claro que, campos de texto estáticos não tem utilizado para usuários. Vamos fazer um exemplo mais prático:

class ControlledNameInput extends React.Component {
constructor() {
super()
this.state = {name: ""}
}

render() {
return <input type="text" value={this.state.name} />
}
}

E para controlar a entrada de dados é uma questão de mudar o estado do componente.

return (
<input
value={this.state.name}
onChange={e => this.setState({ name: e.target.value })}
/>
)

Esta é uma entrada controlada. React apenas atualiza o DOM quando o estado mudar em nosso componente. Isso é inestimável ao criar UI consistentes.

Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store