Desenvolvimento

1 mar, 2017

Lidando com estados em React – Parte 01

Publicidade

O que mais vejo na internet ultimamente, quando o assunto é React, é a dificuldade que os desenvolvedores passam para ter um estado consistente e de fácil manutenção.

Muitas vezes, esta dificuldade resulta em um código aquém do esperado; código este que acaba gerando muitas críticas ao React em si – tecnologia que não tem nada a ver com o problema -, e por essas e outras, decidi criar este guia, que será dividido em duas ou três partes.

Antes de aprender a lidar com estados, precisamos entender exatamente o que são e porque estão aqui. Vejamos o seguinte trecho de código em JavaScript:

let color = 'red'
const askFavoriteColor = () => {
  color = prompt('What color do you like most?') || color
}

askFavoriteColor()
alert(color)

Nele, definimos que color é red e em seguida declaramos uma função askFavoriteColor, que exibe na tela um alerta, perguntando que cor o usuário gosta mais, e caso não tenha resposta, retorna o valor natural de color.

Na sequência, executamos esta função e finalmente mostramos outro alerta exibindo a resposta do usuário.

O que vale observar aqui é que tudo gira em torno de uma mesma variável color que por sua vez, tem o seu valor declarado pelo menos duas vezes na mesma implementação: um inicial, red, e o outro, a resposta do usuário.

E isso é um fragmento de código “stateful” ou “que possui estado”. color possui duas declarações. Ele foi modificado (ou “mutado”, o contrário de “imutado”, que deriva de “imutável” – se aqui foi o “click” para você entender aquele famoso termo que andam usando, “imutabilidade”, me deixe saber) ao decorrer da história.

Enquanto isso…

const color = 'red'
const askFavoriteColor = () => {
  return prompt('What color do you like most?')
}
const favoriteColor = askFavoriteColor()

alert(favoriteColor || color)

… os dois códigos retornam a mesma coisa, mas o fazem de maneiras diferentes. O exemplo acima é stateless; ou, [ele] não possui estado. Vamos transcrever:

  1. Definimos color como red;
  2. Definimos uma função askFavoriteColor cujo retorno é o valor digitado em um alerta que pergunta “What color do you like most?”;
  3. Definimos favoriteColor como sendo o resultado da execução da função askFavoriteColor;
  4. Damos um alerta, exibindo favoriteColor caso este tenha sido declarado; senão, exibimos color, que seria o resultado “padrão”.

O primeiro caso, stateful, é uma característica principalmente de linguagens imperativas ou orientadas a objeto. Stateless, por outro lado, é das linguagens funcionais. Haskell, por exemplo, é completamente stateless. Você absolutamente não consegue redeclarar o valor de variável alguma; o primeiro exemplo, portanto, falharia – teríamos um erro em tempo de compilação.

E onde entra o React?

Você usa ele para escrever componentes, não é mesmo? E a premissa de componentes, não sendo mistério para ninguém, é que criemos códigos isolados, de fácil transporte. Não aquela coisa amarrada, dependente de várias outras variáveis e condicionais externas que dá até um frio na barriga só de pensar em dar manutenção.

Agora, eis aqui um “segredo” a ser revelado: extrairemos o melhor do React se trabalharmos stateless sempre que possível. A mentalidade componentizada da biblioteca do Facebook consiste nisso: para criarmos códigos reusáveis e escaláveis, para termos prazer em programar em React e sentirmos o quão produtivo ele é, idealmente o faríamos escrevendo componentes stateless.

Veja esta página Home:

import React, {Component} from 'react'

class Home extends Component {
  state = {isContactModalOpen: false}

  renderContactModal () {
    return (
      <div id="modal">
        <form>
          <input type="text" placeholder="Your e-mail" />
          
          <textarea placeholder="What are your thoughts?" />
          
          <button onClick={this.setState({isContactModalOpen: false})}>Close</button>
          <button>Send</button>
        </form>
      </div>
    )
  }

  render () {
    const {isContactModalOpen} = this.state
    
    return (
      <div>
        {isContactModalOpen ? this.renderContactModal() : null}

        <h1>Hello world!</h1>

        <hr />

        <p>What a beautiful day to learn about React's state—don't you think?</p>
        
        <hr />
        
        <button onClick={this.setState({isContactModalOpen: true})}>Get in touch</button>
      </div>
    )
  }
}

export default Home

Sejamos sinceros: dá até uma preguiça de ler, não dá? E olha que é só uma página que teoricamente “abre” um Modal ao clicar em um botão cuja chamada é “Get in touch”.

Ainda no exemplo acima, temos um método renderContactModal que retorna uma div com um form dentro. Até agora, apesar de você talvez estar pensando o contrário, não tem absolutamente nada de errado. Quero dizer, se sua aplicação possui apenas um Modal e as suas regras de negócio assim permitem, o exemplo acima é perfeitamente okay. Lembre-se: entregar valor é importante. Seja produtivo. Mas e se quisermos desacoplar o Modal para um componente externo, onde possamos utilizá-lo em mais de um lugar?

Eu “melhorei” um pouco a nossa Home, extraindo o Modal para um componente exclusivo:

import React, {Component} from 'react'

import Modal from '../components/Modal'

class Home extends Component {
  state = {isContactModalOpen: false}

  render () {
    const {isContactModalOpen} = this.state
    
    return (
      <div>
        <Modal isOpen={isContactModalOpen} />

        <h1>Hello world!</h1>

        <hr />

        <p>What a beautiful day to learn about React's state—don't you think?</p>
        
        <hr />
        
        <button onClick={this.setState({isContactModalOpen: true})}>Get in touch</button>
      </div>
    )
  }
}
import React, {Component, PropTypes} from 'react'

class Modal extends Component {
  state = {isOpen: false}

  static propTypes = {
    isOpen: PropTypes.bool
  }

  componentWillMount() {
    this.setState({isOpen: this.props.isOpen})
  }

  renderContent () {
    return (
      <div id="modal">
        <form>
          <input type="text" placeholder="Your e-mail" />
          
          <textarea placeholder="What are your thoughts?" />
          
          <button onClick={this.setState({isOpen: false})}>Close</button>
          <button>Send</button>
        </form>
      </div>
    )
  }

  render () {
    {isOpen} = this.state
    
    return isOpen ? this.renderContent() : null
  }
}

No componente acima, faço algo relativamente comum: definir uma propriedade do estado inicial igual a uma propriedade recebida através do componente. Me refiro a isOpen, que antes do componente ser montado, define no estado homônimo o mesmo valor da propriedade recebida através da declaração do componente. E isto é um “anti-pattern”: não é estranho pensar que o valor de um estado é exatamente o mesmo do passado por uma prop?

Além disso, olha que coisa: quando eu disse que “melhorei um pouco a nossa Home”, na verdade, menti. A única coisa que fiz foi extrair o Modal para um arquivo próprio, porque melhoria que é bom, absolutamente nada. E para falar a verdade, eu só piorei a vida do coitado que for dar manutenção para esse código.

Acha que estou exagerando? Vamos então adicionar este mesmo Modal no blog:

import React, {Component} from 'react'

import Modal from '../components/Modal'

const stories = [
  {title: 'But I will wait for you'},
  {title: 'as long as I need to'},
  {title: 'And if you ever get back to Hackensack'},
  {title: 'I\'ll be here for you'}
]
  
class Blog extends Component {
  constructor () {
    super()
    
    this.renderStory = this.renderStory.bind(this)
  }

  state = {isContactModalOpen: false}
  
  renderStory(story) {
    return <p>{story.title}</p>
  }

  render () {
    const {isContactModalOpen} = this.state
    
    return (
      <div>
        <Modal isOpen={isContactModalOpen} />
        
        {stories.map(this.renderStory)}

        <button onClick={this.setState({isContactModalOpen: true})}>Get in touch</button>
      </div>
    )
  }
}

Alguns problemas:

  1. O seu estado isContactModalOpen, tanto no blog quanto na Home, vão ser sempre true uma vez que o botão Get in touch for clicado, independente se o Modal está aberto ou não. Isso porque o estado variável está dentro do Modal. Pura inconsistência (deu para sacar o trocadilho?);
  2. “Como fecha o Modal?”, algum desenvolvedor pode indagar ao se deparar com esse código. Ele não é declarativo. O renderizamos passando uma prop isOpen, cujo valor é uma variável do estado, sensível ao clique do Get in touch. Mas de novo, como fecha mesmo?

Agora, imagina estes problemas em dois, três ou quatro lugares. Como fica a manutenção? A partir daqui, aquele peso na consciência de termos implementado React e a ideia de que foi uma decisão irresponsável já já vem à tona.

Mas calma!

Agora falando sério, vou melhorar o nosso cenário para valer:

import React, {Component} from 'react'

import Modal from '../components/Modal'

class Home extends Component {
  constructor () {
    super()
    
    this.openModal = this.openModal.bind(this)
    this.closeModal = this.closeModal.bind(this)
  }

  state = {isContactModalOpen: false}
  
  openModal() {
    this.setState({isContactModalOpen: true})
  }
  
  closeModal () {
    this.setState({isContactModalOpen: false})
  }

  render () {
    const {isContactModalOpen} = this.state
    
    return (
      <div>
        {isContactModalOpen && <Modal onClose={this.closeModal} />}

        <h1>Hello world!</h1>

        <hr />

        <p>What a beautiful day to learn about React's state—don't you think?</p>
        
        <hr />
        
        <button onClick={this.openModal}>Get in touch</button>
      </div>
    )
  }
}
import React, {Component, PropTypes} from 'react'

const Modal = ({onClose}) => (
  <div id="modal">
    <form>
      <input type="text" placeholder="Your e-mail" />

      <textarea placeholder="What are your thoughts?" />

      <button onClick={onClose}>Close</button>
      <button>Send</button>
    </form>
  </div>
)

Modal.propTypes = {
  onClose: PropTypes.func
}

export default Modal

Agora sim!

As mudanças foram as seguintes:

  1. Todo o controle de visibilidade do Modal foi abstraído para só e somente só a Home—na qual chamaremos de Container ao invés de Componente, explico logo mais.
  2. O componente Modal foi simplificado para uma sintaxe stateless. Uma função pura que retorna um componente, e só isso. Saiba mais.
  3. O componente Modal recebe agora uma e apenas uma propriedade chamada onClose, mas ele não sabe como ela funciona.

Antes, Modal era um componente stateful. Ele não só tinha estado próprio, como estava diretamente dependente de um estado externo. Isso é bem caótico – cedo ou tarde a dor de cabeça há de vir.

Agora, Modal é um componente stateless. Ele não tem mais estado próprio e tampouco depende de terceiros para existir; ele existe por si só e isso é suficiente para ele mesmo.

h20 é o componente; sólido, líquido ou gasoso é o estado. h2o vai ser h2o – possuindo sempre as suas (mesmas) propriedades -, bem como o Modal: se ele vai ser aberto ao clique de um botão, ao fechar de outro Modal ou depois de 30 segundos que o usuário acessar o site, isso não é importante e não afeta as suas propriedades.

Justamente por esse inteligente desacoplamento do estado é que ganhamos a liberdade de renderizar o Modal por aí sem toda a complexidade que tínhamos antes, apenas tendo que implementar um comportamento para onClose, que se faz necessário porque não é da responsabilidade do nosso componente saber como ou quando ele deve ser fechado. Se colocarmos água num freezer, depois de algum tempo, vira gelo; se esquentarmos esse freezer a ponto de sublimar o gelo, teremos gás. A água, que antes estava no estado sólido, passou para o gasoso. No caso do Modal: inicialmente, fechado. Quando estimulado – o clique de um botão (equivalente ao esquentar do freezer previamente citado), por exemplo -, se abre.

Sobre containers e componentes

Bem nos primórdios do React, tudo era tido como componente. Os estados eram anarquistas – tudo se fazia lá. Organização? Gerenciamento? Pff, besteira! Acredite: praticamente tudo o que você possa imaginar acontecia no estado de um componente.

Fazíamos requisições HTTP dentro do método render e salvávamos o resultado no state, passando informações mil componentes abaixo. Pobre coitado a quem fosse designado uma tarefa para resolver algum problema aí no meio.

Pelos problemas que isso gerava, a maior parte da comunidade passou a perceber os valores dos stateless components – provavelmente, a tecnologia nasceu com essa premissa, mas foi preciso empirismo para amadurecer e passar a palavra adiante – e então começou a enfatizar a prática. Inclusive, como já citado, a biblioteca generosamente deu uma mãozinha para tornar o processo ainda mais simples.

Mas ainda assim: a gente precisa de um estado. Precisamos de informações que vêm do nosso servidor. O Modal, diga-se de passagem, precisa de um estado para ser útil. Porque se for sempre visível, seria um Modal, um Modal?

Sendo assim, separemos os componentes dos containers.

  • Componentes: De modo geral, idiotas, tolos, burros – sim, “dumb (component)”, talvez você já tenha ouvido falar -, ou como você quiser chamar. Geralmente, eles não tem noção de espaço ou estado. Se for um botão, por exemplo, ele não deve se importar em que ponto da tela está sendo renderizado. Quem sabe disso é o container. Ele só se importa consigo mesmo. Exemplos de componentes: Button, Modal, Dropdown, Tooltip.
  • Containers: Responsáveis por empacotar vários componentes em uma tela humana. Eles tem noção de espaço – sabem quando um componente deve aparecer, em qual ordem, com qual frequência, etc, e geralmente têm estado. Geralmente porque pode ser que criemos um site estático com React, sem qualquer tipo de interação.

Agora, um exercício para testarmos se conseguimos perceber de fato a diferença entre um componente e um container: um Lightbox, nos limites dos seus conhecimentos técnicos, seria o quê? Componente ou container? Responderei adiante.

O polêmico this.state

A propriedade em si é um trunfo para qualquer aplicativo, se bem utilizada. E dependendo do tamanho do projeto, sequer precisamos de uma camada complexa de dados.

Na Chute – empresa que trabalho até o momento que escrevo este artigo -, por exemplo, temos um produto, iniciado em 2014 e ainda operando em 2017, de fácil manutenção e que possui toda a parte inteligente gerenciada pelo estado dos próprios containers e componentes. Naturalmente, ao longo do tempo, esse “gerenciamento” evoluiu. Antes não distinguíamos containers de componentes: eram uma coisa só. Hoje, a estrutura das pastas e a abstração dos arquivos estão muito melhores e garantem uma boa experiência para dar manutenção e escalar

Às vezes, pode ser que a necessidade de escalar para uma camada isolada de estado nunca surja – se os problemas são resolvidos com um simples this.setState, por que complicar? -, entretanto, o this.state deve sair dos holofotes quando a aplicação for verdadeiramente mais complexa.

Dado o seguinte app:

<Home>
  <Header />
  
  <Body>
    <Sidebar />
    <Content />
  </Body>

  <Footer />
</Home>

Queremos que o clique de um botão lá na Sidebar exiba um Modal que vai sobrepor a aplicação inteira e vai sobrepor o Header na marcação. Dá para resolvermos assim:

import React, {Component} from 'react'

import {Header, Sidebar, Body, Content, Footer} from './containers'
import Modal from '../components/Modal'

class Home extends Component {
  constructor () {
    super()
    
    this.openModal = this.openModal.bind(this)
    this.closeModal = this.closeModal.bind(this)
  }

  state = {isContactModalOpen: false}
  
  openModal() {
    this.setState({isContactModalOpen: true})
  }
  
  closeModal () {
    this.setState({isContactModalOpen: false})
  }

  render () {
    const {isContactModalOpen} = this.state
    
    return (
      <div>
        {isContactModalOpen && <Modal onClose={this.closeModal} />)
        <Header />
        
        <Body>
          <Sidebar onClickRegisterButton={this.openModal} />
          <Content />
        </Body>
        
        <Footer />
      </div>
    )
  }
}
export default Home
import React from 'react'

const Sidebar = ({onClickRegisterButton}) => (
  <aside>
    <ul><li>Login</li></ul>
    <ul><li onClick={onClickRegisterButton}>Register</li></ul>
  </aside>
)

export default Sidebar

Simples, não? Claro que sim! Enquanto resolver o problema, que assim seja! Entretanto, seremos desafiados quando adicionarmos profundidade ao nosso aplicativo. Por exemplo, ter um Dropdown dentro de Sidebar, e este componente ser o responsável por abrir o Modal.

Segue:

import React from 'react'

import Dropdown from '../components/Dropdown'

const Sidebar = ({onClickRegisterButton}) => (
  <aside>
    <ul><li>Login</li></ul>
    <ul><Dropdown onClickRegisterButton={onClickRegisterButton} /></ul>
  </aside>
)

export default Sidebar
import React from 'react'

const Dropdown = ({onClickRegisterButton}) => (
  <ul>
    <li>One</li>
    <li onClick={onClickRegisterButton}>Two</li>
    <li>Three</li>
  </ul>
)

export default Dropdown

Aí considerando que alguém vai dar manutenção para o Dropdown.jsx, seria esta pessoa auspiciosa o suficiente para acertar de onde onClickRegisterButton está vindo? Até mesmo o autor do código vai se perder – é só uma questão de tempo, acredite.

Sobretudo, apesar da separação de containers e components já ajudarem bastante na organização do estado da aplicação, ainda não temos o suficiente: precisamos de consistência. Precisamos lidar com dados que vem do servidor: usuários, posts, comentários… Informações dinâmicas e assíncronas, que mudam a cada requisição.

Enfim, a camada de dados

Atualmente, a solução mais popular – por ser bastante assertiva – para lidar com dados dinâmicos e/ou com alta densidade de uso, vindo ou não do servidor, e garantir um aplicativo íntegro e consistente, se dá por criar uma camada isolada de dados que será tida como a única fonte de verdade do projeto.

Integridade e consistência: se você for exibir a lista de posts do seu blog na página principal e disponibilizar um botão para remover o artigo ao lado do título de cada um deles, a ação que este botão vai engatilhar o remove da mesma fonte que está sendo utilizada para exibi-los. Imediatamente, após a remoção, você verá aquele artigo sumir – aquela lista não mais contém aquele artigo em específico -, e se houver algum outro lugar que esteja consumindo daquela mesma fonte, ele também será afetado.

Para não estender ainda mais este artigo, vou continuar o assunto na segunda parte, onde falarei com detalhes sobre essa “camada de dados”, bem como estados assíncronos, chegando inclusive a envolver os polêmicos Flux e Redux.

A resolução do problema do Lightbox

Essencialmente, o Lightbox é um componente. E sem uma camada única de dados, ele pode possuir estado para ser reutilizado com mais facilidade – o que não é errado. Inclusive, num caso como esse, eu até incentivaria o fazer stateful. Vejamos duas implementações diferentes:

import React from 'react'

const IMAGES = [1, 2, 3]

const Lightbox = ({imageUrl, onPrev, onNext}) => (
  return (
    <div>
      <button onClick={onPrev}>Previous</button>

      <img src={imageUrl} />

      <button onNext={onNext}>Next</button>
    </div>
  )
)

class Home extends Component {
  constructor () {
    super()

    this.prevImageOnLightbox = this.prevImageOnLightbox.bind(this)
    this.nextImageOnLightbox = this.nextImageOnLightbox.bind(this)
  }

  state = {currentLightboxImage: IMAGES[1]}

  prevImageOnLightbox () {
    this.setState({currentLightboxImage: IMAGES[0]})
  }

  nextImageOnLightbox () {
    this.setState({currentLightboxImage: IMAGES[2]})
  }

  render () {
    const {currentLightboxImage} = this.state

    return (
      <Lightbox
        imageUrl={currentLightboxImage}
        onPrev={this.prevImageOnLightbox}
        onNext={this.nextImageOnLightbox}
      />
    )
  }
}

No caso acima, o Lightbox não sabe que pode existir uma imagem anterior ou próxima. A responsabilidade de navegação é do container Home, que sabe sobre as imagens que o Lightbox vai exibir. Isso só seria eficiente se tivéssemos aquela “camada de dados” que mencionei há pouco -explicarei o porquê no próximo artigo -, caso contrário:

import React from 'react'

class Lightbox extends Component {
  constructor () {
    super()

    this.prev = this.prev.bind(this)
    this.next = this.next.bind(this)
  }

  state = {currentImageUrl: null}

  componentWillMount () {
    const {currentImageUrl} = this.props

    this.setState({currentImageUrl: currentImageUrl})
  }

  prev () {
    const {images} = this.props

    this.setState({currentImageUrl: images[0]})
  }

  next () {
    const {images} = this.props

    this.setState({currentImageUrl: images[2]})
  }

  render () {
    const {currentImageUrl} = this.state

    return (
      <div>
        <button onClick={this.prev}>Previous</button>

        <img src={currentImageUrl} />

        <button onNext={this.next}>Next</button>
      </div>
    )
  }
}

class Home extends Component {
  prevImageOnLightbox () {
    this.setState({currentLightboxImage: IMAGES[0]})
  }

  nextImageOnLightbox () {
    this.setState({currentLightboxImage: IMAGES[2]})
  }

  render () {
    const IMAGES = [1, 2, 3]

    return (
      <Lightbox
        currentImageUrl={IMAGES[1]}
        images={IMAGES}
        onPrev={this.prevImageOnLightbox}
        onNext={this.nextImageOnLightbox}
      />
    )
  }
}

Falávamos de stateless components e do quão eficiente eram. De repente, uma das maneiras de construir a Lightbox se tornou stateful.

Verdade, e isso foi proposital. Deixe-me explicar: há pouco, falei que “essencialmente o Lightbox é um componente”. Obviamente, não queremos reescrever o comportamento de navegação dele a cada vez que formos chamá-lo. Por isso é inteligente passar um array de imagens como propriedade, juntamente com a imagem atual a ser exibida. Dessa forma, nós podemos identificar qual é o ponto de referência para partir com a navegação: se a imagem exibida está no índice 1, sabemos que a anterior vai ser a de índice 0, e a próxima, 2.

O que faço-me responsável por salientar através deste exercício, é que existem vários pequenos casos de estado que não requerem uma tecnologia extra para ter o trabalho feito e com qualidade. Em muitos casos, o state layer do React vai ser suficiente.