React Table é uma biblioteca leve e flexível para renderizar tabelas no React. Neste tutorial, você aprenderá como criar uma tabela classificável e filtrável com React Table e Hooks
Antes de começar, este tutorial pressupõe que você tenha conhecimento básico de HTML, CSS, JavaScript e React. Embora analisemos o projeto passo a passo, não explicaremos em detalhes os conceitos básicos dos métodos de array React ou JavaScript. Também usaremos TypeScript, mas o mesmo pode ser feito sem ele. Dito isso, vamos pular para a codificação.
Para este projeto, usaremos o Vite, uma ferramenta de frontend robusta e popular. Se você ainda não possui um aplicativo React existente, você pode inicializar um novo projeto no Vite usando um dos seguintes comandos dentro do seu terminal:
# Using NPM
npm create vite@latest folder-name -- --template react-ts
# Using Yarn
yarn create vite folder-name --template react-ts
# Using PNPM
pnpm create vite folder-name --template react-ts
# Using Bun
bunx create-vite folder-name --template react-ts
Quando estiver pronto, configure uma nova pasta para o componente Table dentro do projeto React com a seguinte estrutura:
src
├─ components
│ ├─ Table
│ │ ├─ index.ts
│ │ ├─ table.css
│ │ ├─ Table.tsx
├─ App.tsx
Abra Table.tsx e exporte o seguinte, para que possamos verificar o carregamento do componente quando o importarmos:
import './table.css'
export const Table = () => {
return (
<h1>Table component</h1>
)
}
Dentro de index.ts, reexporte o componente usando a seguinte linha:
export * from './Table'
Agora que configuramos os arquivos do componente, vamos verificar se ele carrega importando-o para nosso aplicativo. Neste tutorial, usaremos o componente App. Se você tiver um projeto React existente, poderá importá-lo para o local desejado. Importe o componente Table para seu aplicativo da seguinte forma:
import { Table } from './components/Table'
const App = () => {
return (
<Table />
)
}
export default App
É claro que, para trabalhar na tabela, precisaremos primeiro de alguns dados simulados. Para este tutorial, podemos usar JSON Generator, um serviço gratuito para geração de dados JSON aleatórios. Usaremos o seguinte esquema para gerar os dados:
[
'{{repeat(10)}}',
{
id: '{{index()}}',
name: '{{firstName()}} {{surname()}}',
company: '{{company().toUpperCase()}}',
active: '{{bool()}}',
country: '{{country()}}'
}
]
JSON Generator vem com várias funcionalidades integradas para gerar diferentes tipos de dados. O esquema acima criará uma matriz de objetos com dez objetos aleatórios na forma de:
{
id: 0, // number - Index of the array, starting from 0
name: 'Jaime Wallace', // string - A random name
company: 'UBERLUX', // string - Capitalized random string
active: false, // boolean - either `true` or `false`
country: 'Peru' // string - A random country name
}
Gere uma lista de entradas usando o esquema acima e, em seguida, crie um novo arquivo dentro da src pasta chamada data.ts< a i=3> e exporte o array da seguinte maneira:
export const data = [
{
id: 0,
name: 'Jaime Wallace',
company: 'UBERLUX',
active: false,
country: 'Peru'
},
{ ... },
]
Abra App.tsx e passe esses dados para o componente Table como uma propriedade chamada rows. Geraremos a tabela com base nestes dados:
import { Table } from './components/Table'
+ import { data } from './data'
const App = () => {
return (
- <Table />
+ <Table rows={data} />
)
}
export default App
Agora que configuramos o componente e os dados, podemos começar a trabalhar na tabela. Para gerar dinamicamente a tabela com base nos dados passados, substitua tudo no componente Table pelas seguintes linhas de código:
import { useState } from 'react'
import './table.css'
export const Table = ({ rows }) => {
const [sortedRows, setRows] = useState(rows)
return (
<table>
<thead>
<tr>
{Object.keys(rows[0]).map((entry, index) => (
<th key={index}>{entry}</th>
))}
</tr>
</thead>
<tbody>
{sortedRows.map((row, index) => (
<tr key={index}>
{Object.values(row).map((entry, columnIndex) => (
<td key={columnIndex}>{entry}</td>
))}
</tr>
))}
</tbody>
</table>
)
}
Isso gerará dinamicamente os cabeçalhos das tabelas e as células com base na propriedade rows. Vamos detalhar como funciona. Como vamos classificar e filtrar as linhas, precisamos armazená-las em um estado usando o gancho useState. A propriedade é passada como valor inicial para o gancho.
Para exibir os cabeçalhos da tabela, podemos usar Object.keys na primeira entrada do array, que retornará as chaves do objeto como uma lista de strings:
const rows = [
{
id: 0,
name: 'Jaime Wallace'
},
{ ... }
]
// #1 Turn object properties into an array of keys:
Object.keys(rows[0]) -> ['id', 'name']
// #2 Chain `map` from the array to display the values inside `th` elements:
['id', 'name'].map((entry, index) => (...))
Para exibir as células da tabela, precisamos usar Object.values em cada linha, que retorna o valor de cada chave em um objeto, em oposição a Object.keys. Em detalhes, é assim que exibimos as células da tabela:
const sortedRows = [
{
id: 0,
name: 'Jaime Wallace'
},
{ ... }
]
// #1 Loop through each object in the array and create a `tr` element:
{sortedRows.map((row, index) => (<tr key={index}>...</tr>))}
// #2 Loop through each property of each object to create the `td` elements:
Object.values(row) -> [0, 'Jaime Wallace']
Essa abordagem torna extremamente flexível o uso de qualquer tipo de dados com nosso Table componente, sem precisar reescrever a lógica. Até agora, teremos a seguinte tabela criada usando nosso componente. No entanto, existem alguns problemas com a formatação.
No momento, a coluna active não é exibida. Isso ocorre porque os valores desses campos são booleanos e não são impressos como strings em JSX. Para resolver esse problema, podemos introduzir uma nova função para formatar entradas com base em seus valores. Adicione o seguinte ao componente Table e envolva entry na função no JSX:
const formatEntry = (entry: string | number | boolean) => {
if (typeof entry === 'boolean') {
return entry ? '✅' : '❌'
}
return entry
}
return (
<table>
<thead>...</thead>
<tbody>
{sortedRows.map((row, index) => (
<tr key={index}>
{Object.values(row).map((entry, columnIndex) => (
<td key={columnIndex}>{formatEntry(entry)}</td>
))}
</tr>
))}
</tbody>
</table>
)
A função formatEntry espera uma entrada, que no nosso caso pode ser string, number ou , exibiremos uma cruz vermelha. Usando uma abordagem semelhante, também podemos formatar os títulos das tabelas. Vamos colocá-los em maiúscula com a seguinte função:, exibiremos uma marca de seleção verde e, para valores , ou seja, para valores for um boolean e, em seguida, retorna um valor formatado se typeof entrybooleantruefalse
export const capitalize = (
str: string
) => str?.replace(/\b\w/g, substr => substr.toUpperCase())
Esta função usa uma regex para pegar a primeira letra de cada palavra e transformá-la em maiúscula. Para usar esta função, podemos criar um arquivo utils.ts na raiz da pasta src, exportar esta função e importá-la para nosso Table componente a ser usado da seguinte maneira:
import { capitalize } from '../../utils'
export const Table = ({ rows }) => {
...
return (
<table>
<thead>
<tr>
{Object.keys(rows[0]).map((entry, index) => (
<th key={index}>{capitalize(entry)}</th>
))}
</tr>
</thead>
<tbody>...</tbody>
</table>
)
}
Com base nessas modificações, agora temos uma tabela formatada e construída dinamicamente.
Antes de começarmos a estilizar a tabela e depois adicionar controles, vamos digitar corretamente a propriedade rows. Para isso, podemos criar um arquivo types.ts na raiz da pasta src e exportar tipos personalizados que podem ser reutilizados ao longo do projeto. Crie o arquivo e exporte o seguinte tipo:
export type Data = {
id: number
name: string
company: string
active: boolean
country: string
}[]
Para digitar a propriedade rows no componente Table, basta importar esse tipo e passá-lo para o componente da seguinte maneira:
import { Data } from '../../types'
export type TableProps = {
rows: Data
}
export const Table = ({ rows }: TableProps) => { ... }
Para estilizar todo o componente da tabela, precisaremos apenas de algumas regras. Primeiro queremos definir as cores e bordas, o que podemos fazer usando os seguintes estilos:
table {
width: 100%;
border-collapse: collapse;
}
thead {
text-align: left; /* `thead` is centered by default */
color: #939393;
background: #2f2f2f;
}
th,td {
padding: 4px 6px;
border: 1px solid #505050;
}
Adicione o acima a table.css. Certifique-se de definir border-collapse como collapse em <table> para evitar bordas duplas. Como a tabela ocupa toda a tela, vamos também fazer alguns ajustes e remover as bordas esquerda e direita, pois elas não ficam visíveis de qualquer maneira:
th:first-child,
td:first-child {
border-left: 0;
}
th:last-child,
th:last-child {
border-right: 0;
}
Isso eliminará as bordas de cada lado do <table>, resultando em uma aparência mais limpa. Por fim, vamos adicionar um efeito de foco nas linhas da tabela para ajudar os usuários visualmente ao pesquisar na tabela:
tr:hover {
background: #2f2f2f;
}
Com tudo até agora, agora temos o seguinte comportamento para o componente.
Agora que estilizamos a tabela, vamos adicionar os controles para a funcionalidade de classificação e filtro. Criaremos um elemento <input> para o filtro e um elemento <select> para a classificação. Também incluiremos um botão para alternar entre ordens de classificação (crescente/decrescente).
Para adicionar as entradas, também precisaremos de novos estados para a ordem atual (crescente ou decrescente) e uma variável para controlar a chave de classificação (qual chave no objeto é usada para classificação). Com isso em mente, estenda o componente Table com o seguinte:
const [order, setOrder] = useState('asc')
const [sortKey, setSortKey] = useState(Object.keys(rows[0])[0])
const filter = (event: React.ChangeEvent<HTMLInputElement>) => {}
const sort = (value: keyof Data[0], order: string) => {}
const updateOrder = () => {}
return (
<>
<div className="controls">
<input
type="text"
placeholder="Filter items"
onChange={filter}
/>
<select onChange={(event) => sort()}>
{Object.keys(rows[0]).map((entry, index) => (
<option value={entry} key={index}>
Order by {capitalize(entry)}
</option>
))}
</select>
<button onClick={updateOrder}>Switch order ({order})</button>
</div>
<table>...</table>
</>
)
Vamos lá para entender o que mudou:
Observe que usamos a mesma lógica que usamos para os elementos <th> para gerar dinamicamente as opções para <select>. Também podemos reutilizar a função utilitária capitalize para formatar as opções.
Vamos estilizar os controles antes de prosseguir. Isso pode ser feito com apenas algumas regras CSS. Estenda table.css com o seguinte:
.controls {
display: flex;
}
input,
select {
flex: 1;
padding: 5px 10px;
border: 0;
}
button {
background: #2f2f2f;
color: #FFF;
border: 0;
cursor: pointer;
padding: 5px 10px;
}
Isso garantirá que as entradas estejam alinhadas uma ao lado da outra. Ao usar flex: 1 nos elementos <input> e <select>, podemos fazer com que eles ocupem uma largura igual do espaço disponível. O <button> ocupará todo o espaço necessário para seu texto.
Agora que temos os controles instalados, vamos ver como implementar a funcionalidade. Para filtrar a tabela com base em qualquer campo, precisaremos seguir esta lógica:
const rows = [
{
id: 0,
name: 'Jaime Wallace'
},
{ ... }
]
// #1: Set `rows` to a filtered version using `filter`
// The return value of `filter` will determine which rows to keep
setRows([ ...rows ].filter(row => { ... }))
// From here on, we discuss the return value of `filter`
// #2: Grab every field from the `row` object to use it for filtering
Object.values(row) -> [0, 'Jaime Wallace']
// #3: Join the values together into a single string
[0, 'Jaime Wallace'].join('') -> '0Jaime Wallace'
// #4: Convert the string into lowercase to make search case-insensitive
'0Jaime Wallace'.toLowerCase() -> '0jaime wallace'
// #5: Check if the string contains the value entered in the input
'0jaime wallace'.includes(value) -> true / false
Com tudo combinado, podemos criar o return valor para filter com base na lógica acima. Isso nos deixa com a seguinte implementação para a função filter:
const filter = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
if (value) {
setRows([ ...rows.filter(row => {
return Object.values(row)
.join('')
.toLowerCase()
.includes(value)
}) ])
} else {
setRows(rows)
}
}
Observe que também queremos verificar se value está presente. A sua ausência significa que o campo <input> está vazio. Nesses casos, queremos redefinir o estado e passar o rows não filtrado para setRows para redefinir a tabela.
Temos a funcionalidade de filtro, mas ainda falta a classificação. Para classificação, temos duas funções separadas:
Vamos começar primeiro com a função de classificação. Sempre que <select> mudar, a função sort será chamada. Queremos usar o valor do elemento <select> para decidir qual chave usar para classificação. Para isso, podemos usar um método sort simples e notação de colchetes para comparar dinamicamente as chaves dos objetos:
const sort = (value: keyof Data[0], order: string) => {
const returnValue = order === 'desc' ? 1 : -1
setSortKey(value)
setRows([ ...sortedRows.sort((a, b) => {
return a[value] > b[value]
? returnValue * -1
: returnValue
}) ])
}
Vamos examinar a função de cima para baixo para entender melhor a implementação.
Tomemos o seguinte como exemplo:
const rows = [{ id: 0 }, { id: 1 }]
const value = 'id'
// This translate to a.id and b.id
rows.sort((a, b) => a[value] > b[value] ? -1 : 1)
// If `returnValue` is -1, the order changes
rows.sort((a, b) => a[value] > b[value] ? 1 : -1)
Para atualizar a ordem de classificação, precisamos apenas atualizar o order estado sempre que o botão for clicado. Podemos conseguir isso com a seguinte funcionalidade:
const updateOrder = () => {
const updatedOrder = order === 'asc' ? 'desc' : 'asc'
setOrder(updatedOrder)
sort(sortKey as keyof Data[0], updatedOrder)
}
Isso definirá order para o oposto a cada clique. Observe que depois de atualizarmos o estado order usando setOrder, também precisamos chamar a função sort para recorrer à tabela com base no pedido atualizado. Para inferir o tipo correto para a variável sortKey, podemos referenciar as chaves do tipo Data usando typecasting: as keyof Data[0]. Como segundo parâmetro, também precisamos passar a ordem atualizada.
Para concluir este projeto, vamos adicionar algumas indicações para um estado superfiltrado. Queremos mostrar um estado superfiltrado apenas se não houver resultados. Isso pode ser feito facilmente verificando o length do nosso sortedRows estado. Após o elemento <table>, adicione o seguinte:
return (
<>
<div className="controls">...</div>
<table>...</table>
{!sortedRows.length && (
<h1>No results... Try expanding the search</h1>
)}
</>
)