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:
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:
FilterableProductListenvolve tudo.SearchBarcontém a entrada de texto e a checkbox de produtos em estoque.ProductTablemostra 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.
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:
- Muda ao longo do tempo?
- É 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:
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:
SearchBarprecisa de ambos os valores, porque renderiza a entrada e a checkbox que os mostram.ProductTableprecisa 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:
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:
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.
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.
Próximo: React Acessível, onde a interface que você acabou de desenhar fica utilizável por todos.

