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:
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:
FilterableProductListenvolve tudo.SearchBarcontém a entrada de texto e a checkbox de estoque.ProductTablemostra 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.
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:
- Isso muda ao longo do tempo?
- É 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:
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:
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 consegue entregar estado ao outro. O componente mais próximo que fica acima dos dois é FilterableProductList. É onde 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 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:
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.
Próximo: React Acessível, onde a interface que você acabou de desenhar se torna utilizável por todos.

