Skip to content
This page has been auto-translated and may contain errors.View in English

Pensando em React

Até aqui cada capítulo ensinou uma peça: componentes, props, estado, eventos, listas. Este capítulo é onde tudo se junta em um método. Dado um design, como você decide quais são os componentes, quais valores são estado, e onde esse estado deve viver? Existe uma forma repetível de trabalhar nisso, e uma vez que você fizer algumas vezes, vai usar os mesmos cinco passos em quase todas as telas que construir.

Vamos trabalhar uma pequena interface do começo ao fim: uma lista de produtos com busca. Ela mostra um conjunto de produtos, uma caixa de busca para filtrar por nome, e uma checkbox para esconder produtos fora de estoque. Aqui estão os dados que ela renderiza:

jsx
const PRODUCTS = [
  { id: 1, name: 'Maçã', price: '$1', stocked: true },
  { id: 2, name: 'Pitanga', price: '$1', stocked: false },
  { id: 3, name: 'Morango', price: '$2', stocked: true },
]

Passo 1: Quebra o design em uma árvore de componentes

Olhe para o design e desenhe caixas ao redor das peças. Uma boa caixa é uma coisa que faz um trabalho, o mesmo instinto que você usa ao decidir o que uma função deveria fazer. Nossa pequena tela se divide limpa em algumas:

  • FilterableProductList envolve tudo.
  • SearchBar contém a entrada de texto e a checkbox de produtos em estoque.
  • ProductTable mostra a lista de produtos.
  • ProductRow é uma única linha nessa tabela.

Elas se aninha dentro uma da outra, então formam uma árvore de componentes:

FilterableProductList
├── SearchBar
└── ProductTable
    └── ProductRow  (um por produto)

Não existe uma única forma correta de dividir, e a regra prática do capítulo componentes se aplica aqui: mantenha cada um pequeno e dê a ele um trabalho claro. Uma linha sabe como desenhar um produto; a tabela sabe como organizar linhas; a barra de busca sabe sobre os controles. Se uma peça começa a fazer duas coisas não relacionadas, geralmente é sinal de dividi-la.

Passo 2: Constrói uma versão estática só com props

Agora constrói uma versão que renderiza os dados mas não faz nada ainda. Sem cliques, sem filtros, sem digitação que muda algo. Os dados fluem de uma forma, para baixo na árvore, através de props. Nenhum useState aparece em nenhum lugar neste passo.

jsx
function FilterableProductList({ products }) {
  return (
    <div>
      <SearchBar />
      <ProductTable products={products} />
    </div>
  )
}

function ProductTable({ products }) {
  return (
    <table>
      <tbody>
        {products.map(product => (
          <ProductRow key={product.id} product={product} />
        ))}
      </tbody>
    </table>
  )
}

function ProductRow({ product }) {
  return (
    <tr>
      <td>{product.name}</td>
      <td>{product.price}</td>
    </tr>
  )
}

function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Buscar..." />
      <label>
        <input type="checkbox" /> Mostrar apenas produtos em estoque
      </label>
    </form>
  )
}

Isso renderiza a lista completa toda vez. A caixa de busca e a checkbox estão na tela, mas não fazem nada ainda. Esse é o ponto deste passo: você coloca toda a estrutura funcionando só com props, e confirma que a árvore do passo 1 realmente se sustenta, antes que nenhuma das peças em movimento estejam envolvidas. O array products vem como prop no topo e é passado para baixo; nada aqui lembra ou muda nada.

Passo 3: Encontra o estado mínimo

Agora torna interativo, e a primeira pergunta é quais valores precisam ser estado. Estado é caro para manter correto, então você quer o mínimo possível dele. O resto você vai calcular.

O teste para cada valor é curto. Faça duas perguntas:

  1. Muda ao longo do tempo?
  2. É algo que você não pode calcular de outro valor que já tem?

Se ambas são sim, é estado. Se uma é não, não é. Caminhe pelos valores na nossa UI através daquele teste:

  • A lista de produtos. Ela é passada e não muda enquanto o usuário interage com a página, então não é estado. É uma prop.
  • O texto de busca que o usuário digitou. Muda ao longo do tempo, e não há nada para calculá-lo. É estado.
  • Se a checkbox de produtos em estoque está ligada. Mesma história: muda, e nada a deriva. É estado.
  • A lista filtrada mostrada de verdade na tela. Muda, mas você pode computá-la a partir dos produtos, do texto de busca e da checkbox. Então não é estado.

Aquele último é a regra que vale a pena gravar: derive, não duplique. A lista visível é os produtos passados pelos filtros atuais, então você a calcula durante a renderização em vez de guardar uma cópia segunda em estado:

jsx
const visible = products.filter(product => {
  const matchesText = product.name
    .toLowerCase()
    .includes(filterText.toLowerCase())
  const matchesStock = !inStockOnly || product.stocked
  return matchesText && matchesStock
})

Se você tivesse guardado visible em seu próprio useState, você teria que lembrar de atualizá-lo toda vez que os produtos, o texto ou a checkbox mudassem. Perca uma e a tela mostra uma lista obsoleta. Derivá-la significa que há só uma fonte de verdade, e nunca pode ficar dessincronizada, porque é recomputada fresca a cada renderização. Então o estado mínimo aqui é exatamente dois valores: filterText e inStockOnly.

Passo 4: Decide onde cada peça de estado deve viver

Você tem duas peças de estado. Agora descubra qual componente dono de cada uma é. A regra é pôr estado no pai comum mais próximo de todo componente que precisa dele, que é a mesma ideia que o capítulo levantando estado entra em profundidade.

Trabalhe quem precisa de cada valor:

  • SearchBar precisa de ambos os valores, porque renderiza a entrada e a checkbox que os mostram.
  • ProductTable precisa de ambos também, porque filtra as linhas usando-os.

SearchBar e ProductTable são irmãos. Um valor só pode fluir para baixo através de props, então nenhum irmão pode passar estado para o outro. O componente mais próximo que fica acima dos dois é FilterableProductList. É lá que o estado vai:

jsx
import { useState } from 'react'

function FilterableProductList({ products }) {
  const [filterText, setFilterText] = useState('')
  const [inStockOnly, setInStockOnly] = useState(false)

  return (
    <div>
      <SearchBar
        filterText={filterText}
        inStockOnly={inStockOnly}
        onFilterTextChange={setFilterText}
        onInStockOnlyChange={setInStockOnly}
      />
      <ProductTable
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly}
      />
    </div>
  )
}

O estado vive no pai, e os valores fluem para ambos os filhos como props. ProductTable os lê para construir sua lista visible; SearchBar os lê para preencher a entrada e a checkbox.

Passo 5: Adiciona a interação que atualiza o estado

Os valores chegam aos filhos, mas o usuário ainda não pode mudá-los. Os dados só fluem para baixo, então para enviar uma mudança de volta para cima você passa os setters para baixo como props e deixa o filho chamá-los. FilterableProductList já passa onFilterTextChange e onInStockOnlyChange acima. SearchBar os conecta às entradas:

jsx
function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange,
}) {
  return (
    <form>
      <input
        type="text"
        placeholder="Buscar..."
        value={filterText}
        onChange={e => onFilterTextChange(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={inStockOnly}
          onChange={e => onInStockOnlyChange(e.target.checked)}
        />{' '}
        Mostrar apenas produtos em estoque
      </label>
    </form>
  )
}

Agora o loop está fechado. Digitar na caixa chama onFilterTextChange, que é setFilterText lá no pai. Isso atualiza o estado, o pai re-renderiza, o novo filterText flui de volta para ambos os filhos, ProductTable recomputada sua lista visible, e a tela bate com o que o usuário digitou. A checkbox funciona do mesmo jeito. O estado desce como um valor, as mudanças voltam para cima como uma chamada de função, e a lista derivada segue junto por conta própria.

Esse é o método inteiro: desenhe a árvore de componentes, construa-a estática com props, encontre o estado mínimo, levante-o para o dono certo, daí conecte a interação. Quase qualquer tela que você encontre cede àqueles cinco passos.

O passo 3 é aquele que recompensa cuidado, porque "mínimo" é mais rigoroso que parece à primeira vista. A pergunta de trabalho é: qual é o menor conjunto de valores a partir do qual todo outro valor na tela pode ser recomputado? Para nossa lista esse é filterText e inStockOnly, e tudo visível é uma função pura desses dois mais o products que chega. Qualquer coisa que você possa derivar durante a renderização, você deveria. A lista filtrada é o caso mais claro, mas a mesma lógica elimina guardar um count de correspondências, um boolean hasResults, ou uma cópia ordenada do array. Cada um desses é uma função de estado que você já tem, então manter em seu próprio useState compra você um segundo valor para manter em sincronia e um novo jeito de estar errado. Valores derivados não podem ficar obsoletos, porque não persistem; são recalculados a cada renderização da única fonte de verdade.

A troca para vigiar no outro lado é derivar algo caro a cada renderização. Alcance useMemo só quando perfil mostra um custo real. O padrão permanece: derive durante a renderização, e promova um valor a estado só quando muda ao longo do tempo e nada o produz.

O passo 1 tem seu próprio julgamento, e corta nos dois sentidos. Divida pouco demais e um componente cresce em um emaranhado que é difícil de reusar ou raciocinar sobre. Divida demais e você consegue um spray de componentes minúsculos encadeando props através de camadas que existem só para passá-los, que é seu próprio tipo de difícil de seguir. O teste útil é responsabilidade e reuso: um limite ganha seu lugar quando a peça tem um trabalho claro, quando repete (um ProductRow renderizado uma vez por item), ou quando puxá-lo torna o pai legível. Um limite que só renderiza em um lugar e encaminha suas props intocadas normalmente é um que você pode colocar inline. Deixe duplicação real e complexidade real puxar componentes além, em vez de dividir no palpite que você vai precisar das costuras depois.

JunoCinco passos do design para React Aqui está a receita que você pode contar: desenhe caixas ao redor das peças para conseguir seus componentes, construa com props primeiro para que mostre os dados e nada mais, daí encontre os poucos valores que realmente mudam e não podem ser exercidos de outra coisa, esses são seu estado.

Coloque cada um no componente mais próximo acima de tudo que precisa dele, e passe um setter para baixo para que um clique ou uma digitação possa atualizá-lo.

A lista que você mostra na tela não é estado; você a calcula a partir dos produtos e dos filtros toda vez.

JunoCinco passos do design para React O método é o mesmo toda vez: árvore de componentes, versão estática só com props, estado mínimo, levante para o pai comum mais próximo, conecte a interação.

A pergunta de estado é a afiada, se um valor muda ao longo do tempo e você não pode derivá-lo, é estado, senão computa durante a renderização.

Derive, não duplique: guarde filterText e inStockOnly, e calcule a lista visível a partir deles para que nunca possa ficar dessincronizada.

JunoCinco passos do design para React Estado mínimo significa o menor conjunto do qual tudo mais é uma função pura de; aqui, filterText e inStockOnly, com a lista filtrada, contagens, e flags todas derivadas durante a renderização em vez de guardadas. Estado extra é outro jeito de dessincronia, e useMemo ganha seu lugar só uma vez que perfil mostra um custo real.

Limites de componentes são o outro julgamento: divida por um trabalho real, repetição, ou legibilidade, e coloque inline os wrappers pass-through que você adicionou na especulação.

Próximo: React Acessível, onde a interface que você acabou de desenhar fica utilizável por todos.