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

Pensando em React

Cada capítulo até agora ensinou uma peça: componentes, props, state, 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 um jeito repetível de trabalhar isso, e uma vez que você faça algumas vezes, vai usar os mesmos cinco passos em quase toda tela que construir.

Vamos caminhar uma pequena interface de ponta a ponta: uma lista de produtos pesquisável. Ela mostra um conjunto de produtos, uma caixa de busca para filtrá-los por nome e uma checkbox para esconder qualquer coisa que não tem estoque. Aqui estão os dados que ela renderiza:

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

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

Olhe 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 deve fazer. Nossa telinha se divide claramente em alguns:

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

Eles se encaixam um dentro do outro, então formam uma árvore de componentes:

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

Não há uma única divisão correta, 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 é um sinal para dividi-la.

Passo 2: Construa uma versão estática com apenas props

Agora construa uma versão que renderiza os dados mas não faz nada ainda. Sem cliques, sem filtros, nada que mude quando você digita. Os dados fluem de um jeito, descendo a árvore, através de props. Nenhum useState aparece em lugar nenhum nesse 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="Pesquisar..." />
      <label>
        <input type="checkbox" /> Mostrar apenas produtos em estoque
      </label>
    </form>
  )
}

Isso renderiza a lista inteira sempre. A caixa de busca e a checkbox estão na tela, mas não fazem nada ainda. Esse é o ponto do passo: você consegue o layout inteiro funcionando apenas com props, e confirma que a árvore do passo 1 realmente se encaixa, antes de qualquer uma das partes móveis ser envolvida. O array products vem como prop no topo e desce; nada aqui lembra ou muda nada.

Passo 3: Encontre o estado mínimo

Agora torne isso interativo, e a primeira pergunta é quais valores precisam ser estado de verdade. Estado é caro de manter correto, então você quer o mínimo possível dele. O resto você calcula.

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

  1. Isso muda ao longo do tempo?
  2. É algo que você não consegue calcular a partir de outro valor que você já tem?

Se ambas forem sim, é estado. Se uma for não, não é. Passe os valores da nossa interface por esse teste:

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

Esse último é a regra que vale gravar: derive, não duplique. A lista visível é os produtos passados através dos filtros atuais, então você calcula durante a renderização em vez de guardar uma segunda cópia 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, teria que lembrar de atualizá-la toda vez que os produtos, o texto ou a checkbox mudassem. Deixe passar um e a tela mostra uma lista obsoleta. Derivá-la significa que há apenas uma fonte de verdade, e nunca pode sair de sincronismo, porque é recalculada fresca em cada renderização. Então o estado mínimo aqui é exatamente dois valores: filterText e inStockOnly.

Passo 4: Decida onde cada pedaço de estado deve viver

Você tem dois pedaços de estado. Agora descubra qual componente é dono de cada um. A regra é colocar estado no componente pai mais próximo de todos que precisam dele, que é a mesma ideia que o capítulo lifting state up entra em profundidade.

Descubra 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 consegue entregar estado ao outro. O componente mais próximo que fica acima dos dois é FilterableProductList. É onde 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 baixo aos dois filhos como props. ProductTable os lê para construir sua lista visible; SearchBar os lê para preencher a entrada e a checkbox.

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

Os valores chegam aos filhos, mas o usuário ainda não consegue 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="Pesquisar..."
        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 fechou. 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 recalcula sua lista visible, e a tela corresponde ao que o usuário digitou. A checkbox funciona do mesmo jeito. O estado desce como valor, mudanças sobem como chamada de função, e a lista derivada a acompanha sozinha.

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, depois conecte a interação. Quase qualquer tela que você encontra cede a esses cinco passos.

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

A compensação para observar do outro lado é derivar algo caro em cada renderização. Recorra a useMemo apenas quando o profile mostra um custo real. O padrão permanece: derive durante a renderização, e promova um valor a estado apenas quando ele muda ao longo do tempo e nada o produz.

O passo 1 tem seu próprio julgamento, e corta dos dois lados. Divida muito pouco e um componente cresce em um emaranhado que é difícil de reutilizar ou raciocinar. Divida demais e você tem um spray de componentes minúsculos passando props através de camadas que existem só para passá-los adiante, que é sua própria espécie de difícil de acompanhar. O teste útil é responsabilidade e reutilização: uma fronteira ganha seu lugar quando a peça tem um trabalho claro, quando se repete (um ProductRow renderizado uma vez por item), ou quando puxá-la torna o pai legível. Uma fronteira que só renderiza em um spot e passa suas props intocadas geralmente é uma que você consegue colocar inline. Deixe duplicação real e complexidade real puxarem componentes para longe, em vez de dividir no palpite de que você precisará das costuras depois.

JunoCinco passos de design para React Aqui está a receita em que você pode contar: desenhe caixas ao redor das peças para ter seus componentes, construa com props primeiro para que mostre os dados e nada mais, depois encontre os poucos valores que realmente mudam e não conseguem ser deduzidos 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 um pressionamento de tecla consiga atualizá-lo. A lista que você mostra na tela não é estado; você a calcula a partir dos produtos e dos filtros sempre.
JunoCinco passos de design para React O método é o mesmo toda vez: árvore de componentes, versão estática com apenas props, estado mínimo, levante para o pai comum mais próximo, conecte a interação. A pergunta de estado é a aguda, se um valor muda ao longo do tempo e você não consegue derivá-lo, é estado, senão calcule durante a renderização. Derive, não duplique: guarde filterText e inStockOnly, e calcule a lista visível a partir deles para que nunca saia de sincronismo.
JunoCinco passos de design para React Estado mínimo significa o menor conjunto a partir do qual tudo mais é uma função pura; aqui, filterText e inStockOnly, com a lista filtrada, contagens e flags todos derivados durante a renderização em vez de guardados. Estado extra é outro jeito de desincronizar, e useMemo ganha seu lugar apenas uma vez que profiling mostra um custo real. Fronteiras 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 especificação.

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