React Table & Data Management
This comprehensive guide covers the essentials of React Table, including setup, data management, table features, styling, optimization, and best practices. Perfect for beginners looking to master data management in React with React Table.
Introduction to React Table
What is React Table?
React Table is a powerful, flexible, and extensible library for building complex and interactive tables in React applications. Think of it like a Swiss Army knife for data tables, offering a wide range of features that can be customized to fit almost any use case. Whether you're building a simple leaderboard or a sophisticated data analysis tool, React Table provides the tools you need.
Key Features of React Table
- Declarative Syntax: React Table uses a declarative syntax that makes it easy to define and manage your table's structure. This means you describe what you want, and React Table handles the rest.
- State Management Hooks: It provides a wide range of hooks for managing table state such as sorting, filtering, pagination, and more, which can be easily integrated into your application.
- Virtualization: For large datasets, React Table supports virtualization, allowing it to render only the visible parts of the table, which greatly enhances performance.
- Plugin Architecture: React Table is highly modular and extensible. You can start with the core library and add plugins for additional features like sorting, filtering, and pagination as needed.
- Customizable Styling: You can style your tables using inline styles, CSS, CSS modules, or libraries like Styled Components.
Why Use React Table?
- Simplicity: React Table is built on React's principles, making it easy to integrate into existing React applications.
- Performance: It's optimized for performance, even with large datasets.
- Flexibility: You can customize almost every aspect of the table to fit your specific needs.
- Community and Ecosystem: Being a popular library, React Table has a large community and a variety of resources for learning and troubleshooting.
Setting Up React Table
Installation
Prerequisites
Before you start using React Table, ensure you have the following:
- Node.js and npm: Make sure you have Node.js and npm installed on your machine. You can check this by running
node -v
andnpm -v
in your terminal. - React Application: You should have a React application set up. You can create one using
npx create-react-app your-app-name
.
Installing React Table
To install React Table, navigate to your React project's directory and run the following command in your terminal:
npm install react-table
This command will add React Table and its necessary dependencies to your project.
Basic Setup
Importing React Table
Once React Table is installed, you can import the necessary hooks and components in your React components. For a simple table setup, you will primarily need useTable
:
import { useTable } from 'react-table'
Basic Table Structure
Here's a basic example of how to set up a React Table component:
import React from 'react'
import { useTable } from 'react-table'
// Sample data
const data = React.useMemo(
() => [
{ col1: 'Hello', col2: 'World' },
{ col1: 'React', col2: 'Table' },
{ col1: 'Data', col2: 'Grid' },
],
[]
)
// Sample column configuration
const columns = React.useMemo(
() => [
{ Header: 'Column 1', accessor: 'col1' },
{ Header: 'Column 2', accessor: 'col2' },
],
[]
)
function TableComponent() {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
)
}
export default TableComponent
Explanation of the Example:
- Data: We define some sample data using the
React.useMemo
hook to prevent unnecessary re-renders. - Columns: We define columns using the
React.useMemo
hook. Each column has aHeader
and anaccessor
that maps to the property of the data object. - useTable Hook: We pass the
columns
anddata
to theuseTable
hook, which provides several functions and properties needed to render the table. - Rendering the Table: We render the table using the properties returned from
useTable
. We loop throughheaderGroups
to create the table headers and loop throughrows
to create the table body rows.
Data Management Basics
Understanding Data Sources
Static Data
Static data is data that doesn't change during the application's lifecycle. It's ideal for small datasets that don't need to be fetched from an external source.
Here's an example of a React Table component using static data:
import React from 'react'
import { useTable } from 'react-table'
const staticData = [
{ col1: 'Apple', col2: 'Red' },
{ col1: 'Banana', col2: 'Yellow' },
{ col1: 'Grapes', col2: 'Purple' },
]
const staticColumns = [
{ Header: 'Fruit', accessor: 'col1' },
{ Header: 'Color', accessor: 'col2' },
]
function StaticTableComponent() {
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns: staticColumns, data: staticData })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
)
}
export default StaticTableComponent
Explanation of the Example:
- Static Data:
staticData
is a static array of objects that represents the data for the table. This data does not change during the application's runtime. - Static Columns:
staticColumns
defines the columns for the table. Each column object includes aHeader
and anaccessor
. Theaccessor
tells React Table which part of the data object to render in each cell. - Rendering: The rest of the component is similar to the previous example. We use the properties returned from
useTable
to render the table headers and body.
Dynamic Data from APIs
Dynamic data is data that can change and is usually fetched from an external source like an API. Let's see how to fetch and display data from an API using React Table.
Here's an example of fetching data from an API and displaying it in a table:
import React, { useState, useEffect } from 'react'
import { useTable } from 'react-table'
const dynamicColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function DynamicTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns: dynamicColumns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
)
}
export default DynamicTableComponent
Explanation of the Example:
- Dynamic Columns: Similar to the static data example,
dynamicColumns
defines the columns for the table. - State Management: We use the
useState
hook to manage the data state and theuseEffect
hook to fetch data from an API when the component mounts. - Data Fetching: Inside the
useEffect
hook, we fetch data from the JSONPlaceholder API and store it in thedata
state. - Rendering: The rest of the component is similar to the static data example. We use the
useTable
hook to render the table with the fetched data.
State Management
Hooks for State Management
React's built-in hooks like useState
and useEffect
are very useful for state management in React Table. We used these hooks in the dynamic data example to fetch and store data.
Context API
For larger applications, you might want to use the Context API to manage state globally. This can be beneficial when you need to share state across multiple components without prop drilling.
Here's a simplified example of using the Context API:
import React, { createContext, useContext, useState, useEffect } from 'react'
import { useTable } from 'react-table'
const DataContext = createContext()
function DataProvider({ children }) {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
return <DataContext.Provider value={data}>{children}</DataContext.Provider>
}
const columns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function ContextTableComponent() {
const data = useContext(DataContext)
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
)
}
function App() {
return (
<DataProvider>
<ContextTableComponent />
</DataProvider>
)
}
export default App
Explanation of the Example:
- DataContext: This is a context that holds the data. The
DataProvider
component fetches data from the API and provides it to the context. - ContextTableComponent: This component uses the
useContext
hook to consume the data from the context and renders it using React Table. - App Component: The
App
component wraps theContextTableComponent
with theDataProvider
, making the data available to it.
Data Fetching
Fetching Data with useEffect
We used useEffect
to fetch data from an API. Here’s a closer look at how it works:
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
Explanation:
- useState Hook: We use
useState
to create a state variabledata
to store the fetched data. - useEffect Hook: This hook runs the function inside it when the component mounts. It fetches data from the API and updates the
data
state with the fetched data.
Handling Async Data
Handling asynchronous data fetching is crucial for real-world applications. Let's enhance the dynamic data example to handle loading and error states:
import React, { useState, useEffect } from 'react'
import { useTable } from 'react-table'
const dynamicColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function DynamicTableComponent() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
setData(data)
setLoading(false)
})
(error => {
setError(error)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns: dynamicColumns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
)
}
export default DynamicTableComponent
Explanation of the Example:
- Loading and Error State: We add
loading
anderror
state variables to handle the loading and error states. Initially,loading
is set totrue
, anderror
is set tonull
. - useEffect Hook: Inside the
useEffect
hook, we setloading
totrue
before fetching data. After data is fetched successfully, we update thedata
state and setloading
tofalse
. If an error occurs during the fetch, we update theerror
state and setloading
tofalse
. - Conditional Rendering: We return a loading message if
loading
istrue
and an error message iferror
is notnull
.
Table Features
Basic Column Configuration
Defining Columns
Columns in React Table are defined as an array of column objects. Each object can have properties like Header
and accessor
.
Here’s an example:
const columns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
Explanation:
- Header: The
Header
property is used to define the header text for each column. - Accessor: The
accessor
property is used to specify which property of the data object should be rendered in each column.
Column Options
Columns can have many additional options that allow you to customize their behavior and appearance. Here’s an example with some column options:
const columns = [
{ Header: 'ID', accessor: 'id', disableSortBy: true },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email', Cell: ({ value }) => <a href={`mailto:${value}`}>{value}</a> },
]
Explanation:
- disableSortBy: This option disables sorting for the "ID" column.
- Cell: This option allows you to customize the content of each cell. In this example, we render the email as a clickable mailto link.
Sorting and Filtering
Sorting
React Table makes sorting very easy. You can enable sorting by adding the disableSortBy
property to the column configuration. By default, all columns are sortable.
Here’s an example of a sortable table:
import React, { useState, useEffect } from 'react'
import { useTable, useSortBy } from 'react-table'
const sortableColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function SortableTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns: sortableColumns, data }, useSortBy)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
)
}
export default SortableTableComponent
Explanation of the Example:
- useSortBy Hook: We import and use the
useSortBy
hook to enable sorting. - Sorting Indicators: We add sorting indicators (🔼 and 🔽) to the headers to show the sorting state.
- getSortByToggleProps: This property is used to handle sorting when the header is clicked.
Filtering
Filtering allows users to narrow down the data based on specific criteria. Here’s an example of a filterable table:
import React, { useState, useEffect } from 'react'
import { useTable, useFilters } from 'react-table'
const filterableColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function FilterTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
setFilter,
} = useTable({ columns: filterableColumns, data }, useFilters)
return (
<div>
<input
type="text"
value={state.filters?.name || ''}
onChange={e => {
setFilter('name', e.target.value || undefined) // Set undefined to remove the filter entirely
}}
placeholder="Search by name"
/>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
export default FilterTableComponent
Explanation of the Example:
- useFilters Hook: We import and use the
useFilters
hook to enable filtering. - Input Field: We add an input field that allows users to enter a search term. The input field's value is controlled by the
name
filter state. - setFilter: This function is used to update the filter state based on the user's input.
Pagination
Implementing Pagination
Pagination is essential for handling large datasets. React Table provides a usePagination
hook that can be used to add pagination to your table.
Here’s an example of a paginated table:
import React, { useState, useEffect } from 'react'
import { useTable, usePagination } from 'react-table'
const paginatedColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function PaginatedTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
page, // Instead of using 'rows', we'll use page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable({ columns: paginatedColumns, data }, usePagination)
return (
<div>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
<div>
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'<<'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'<'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'>'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'>>'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page: {' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
)
}
export default PaginatedTableComponent
Explanation of the Example:
- usePagination Hook: We import and use the
usePagination
hook to enable pagination. - Pagination Buttons and Input: We add buttons to navigate between pages and an input field to go to a specific page.
- Page Size Selection: We add a dropdown to allow users to choose the number of rows per page.
Customizing Pagination
You can customize the pagination controls to better fit your application's design. Here’s an example of custom pagination controls:
<div>
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'Previous'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'Next'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
min={1}
max={pageOptions.length}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
Explanation of the Example:
- Custom Pagination Controls: We added custom pagination controls using buttons and an input field to navigate between pages and select the page size.
- Min and Max Attributes: We added
min
andmax
attributes to the input field to restrict the user from entering invalid page numbers.
Adding Editable Rows
Making Rows Editable
React Table doesn't natively support editing rows, but you can add this functionality by managing the editing state and handling input changes.
Here’s an example of a simple editable table:
import React, { useState, useEffect } from 'react'
import { useTable } from 'react-table'
const editableColumns = [
{ Header: 'ID', accessor: 'id', disableSortBy: true, disableFilters: true },
{
Header: 'Name',
accessor: 'name',
Cell: EditableCell,
},
{
Header: 'Email',
accessor: 'email',
Cell: EditableCell,
},
]
function EditableCell({ value: initialValue }) {
const [value, setValue] = useState(initialValue)
const [isEditing, setIsEditing] = useState(false)
const inputRef = React.useRef(null)
const handleSave = () => {
setIsEditing(false)
}
React.useEffect(() => {
if (isEditing) {
inputRef.current.focus()
}
}, [isEditing])
if (!isEditing) {
return (
<span
{...{
onClick: () => setIsEditing(true),
}}
>
{initialValue}
</span>
)
}
return (
<input
ref={inputRef}
type="text"
value={value}
onChange={e => {
setValue(e.target.value)
}}
onBlur={handleSave}
style={{
width: '100%',
height: '100%',
border: 'none',
outline: 'none',
backgroundColor: 'lightblue',
}}
/>
)
}
function EditableTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns: editableColumns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
)
}
export default EditableTableComponent
Explanation of the Example:
- EditableCell Component: This component handles the editing logic. It uses
useState
to manage the cell's value andisEditing
to manage the editing state. - Handling Save: The
handleSave
function setsisEditing
tofalse
when the user clicks outside the input field. - Editable Columns: We define the
Cell
property in theeditableColumns
configuration to useEditableCell
for the "Name" and "Email" columns.
Saving Changes
To save changes, you need to handle the user input and update the data state. Here’s how you can do it:
function EditableCell({ value: initialValue, row, column }) {
const [value, setValue] = useState(initialValue)
const [isEditing, setIsEditing] = useState(false)
const inputRef = React.useRef(null)
const handleSave = () => {
const updatedData = data.map(rowData =>
rowData.id === row.original.id ? { ...rowData, [column.id]: value } : rowData
)
setData(updatedData)
setIsEditing(false)
}
React.useEffect(() => {
if (isEditing) {
inputRef.current.focus()
}
}, [isEditing])
if (!isEditing) {
return (
<span
{...{
onClick: () => setIsEditing(true),
}}
>
{initialValue}
</span>
)
}
return (
<input
ref={inputRef}
type="text"
value={value}
onChange={e => {
setValue(e.target.value)
}}
onBlur={handleSave}
style={{
width: '100%',
height: '100%',
border: 'none',
outline: 'none',
backgroundColor: 'lightblue',
}}
/>
)
}
Explanation of the Example:
- Editing Logic: When a cell is clicked,
isEditing
is set totrue
, and the cell is rendered as an input field. - Save Logic: The
handleSave
function updates thedata
state with the new value and setsisEditing
back tofalse
.
Pagination
Implementing Pagination
Pagination in React Table is achieved by using the usePagination
hook as shown in the previous example. This hook provides several properties and functions like canPreviousPage
, canNextPage
, pageOptions
, pageCount
, gotoPage
, nextPage
, previousPage
, and setPageSize
.
Customizing Pagination
You can customize the pagination UI to fit your application's design. Here’s an example of custom pagination controls:
<div>
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'Previous'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'Next'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
min={1}
max={pageOptions.length}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
Explanation of the Example:
- Pagination Controls: We add buttons to navigate between pages and an input field to go to a specific page.
- Page Size Selection: We add a dropdown to select the number of rows per page.
Adding Editable Rows
Making Rows Editable
To make rows editable, you can create a custom cell component that handles the editing logic and integrates it with your data state.
Saving Changes
When a cell is edited, you need to update the data state. Here’s how you can do it:
function EditableCell({ value: initialValue, row, column }) {
const [value, setValue] = useState(initialValue)
const [isEditing, setIsEditing] = useState(false)
const inputRef = React.useRef(null)
const handleSave = () => {
const updatedData = data.map(rowData =>
rowData.id === row.original.id ? { ...rowData, [column.id]: value } : rowData
)
setData(updatedData)
setIsEditing(false)
}
React.useEffect(() => {
if (isEditing) {
inputRef.current.focus()
}
}, [isEditing])
if (!isEditing) {
return (
<span
{...{
onClick: () => setIsEditing(true),
}}
>
{initialValue}
</span>
)
}
return (
<input
ref={inputRef}
type="text"
value={value}
onChange={e => {
setValue(e.target.value)
}}
onBlur={handleSave}
style={{
width: '100%',
height: '100%',
border: 'none',
outline: 'none',
backgroundColor: 'lightblue',
}}
/>
)
}
Explanation of the Example:
- Editing Logic: When a cell is clicked,
isEditing
is set totrue
, and the cell is rendered as an input field. - Save Logic: The
handleSave
function updates thedata
state with the new value and setsisEditing
back tofalse
.
Styling and Customization
Basic Styling
Inline Styles
You can apply inline styles directly to your table elements:
<table {...getTableProps()} style={{ border: '1px solid black' }}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()} style={{ border: '1px solid black' }}>
{column.render('Header')}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()} style={{ border: '1px solid black' }}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
Explanation of the Example:
- Inline Styles: We add inline styles to the table, headers, and rows.
CSS Modules
You can use CSS modules to apply styles to your table:
/* TableModule.module.css */
table {
border: 1px solid black;
}
th {
border: 1px solid black;
}
td {
border: 1px solid black;
}
import styles from './TableModule.module.css'
import React from 'react'
import { useTable } from 'react-table'
function StyledTableComponent() {
// ... (previous code)
return (
<table {...getTableProps()} className={styles.table}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()} className={styles.th}>
{column.render('Header')}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()} className={styles.td}>
{cell.render('Cell')}
</td>
))}
</tr>
)
})}
</tbody>
</table>
)
}
export default StyledTableComponent
Explanation of the Example:
- CSS Modules: We import the CSS module and apply the styles to the table, headers, and rows.
Styled Components
You can use libraries like Styled Components to style your table:
import styled from 'styled-components'
import React, { useState, useEffect } from 'react'
import { useTable } from 'react-table'
const Styles = styled.div`
padding: 1rem;
table {
border-spacing: 0;
border: 1px solid black;
tr {
:last-child {
td {
border-bottom: 0;
}
}
}
th,
td {
margin: 0;
padding: 0.5rem;
border-bottom: 1px solid black;
border-right: 1px solid black;
:last-child {
border-right: 0;
}
}
}
`
const paginatedColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function StyledTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
} = useTable({ columns: paginatedColumns, data }, usePagination)
return (
<Styles>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
<div>
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'First'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'Previous'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'Next'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'Last'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
min={1}
max={pageOptions.length}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</Styles>
)
}
export default StyledTableComponent
Explanation of the Example:
- Styled Components: We use Styled Components to style the table.
- Pagination Controls: We add custom pagination controls using buttons, an input field, and a dropdown.
Advanced Customization
Custom Components for Cells
You can use custom components to render complex cells. Here’s an example:
const customColumns = [
{ Header: 'ID', accessor: 'id' },
{
Header: 'Name',
accessor: 'name',
Cell: ({ value }) => <strong>{value}</strong>,
},
{
Header: 'Email',
accessor: 'email',
Cell: ({ value }) => <a href={`mailto:${value}`}>{value}</a>,
},
]
function CustomTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns: customColumns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
)
}
export default CustomTableComponent
Explanation of the Example:
- Custom Cells: We define custom cells for the "Name" and "Email" columns.
- Rendering: The "Name" column is rendered in bold, and the "Email" column is rendered as a clickable mailto link.
Dynamic Styling Based on Data
You can dynamically style table cells based on the data. Here’s an example:
const styledColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function StyledCell({ value, column }) {
const style = {
backgroundColor: value.includes('a') ? 'lightgreen' : 'transparent',
}
return <span style={style}>{value}</span>
}
function StyledTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns: styledColumns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()} style={cell.column.id === 'name' ? { backgroundColor: 'lightblue' } : {}}>
{cell.render('Cell', { StyledCell })}
</td>
))}
</tr>
)
})}
</tbody>
</table>
)
}
export default StyledTableComponent
Explanation of the Example:
- StyledCell Component: This component applies styles based on the cell value.
- Conditional Styling: We apply a light blue background to cells in the "Name" column.
Advanced Topics
Integrating with State Management Libraries
Redux
Integrating React Table with Redux allows you to manage table state globally. Here’s a basic example:
- Install Redux:
npm install @reduxjs/toolkit react-redux
- Set Up Redux:
Create a store
and a slice
for managing the table data.
import { configureStore, createSlice } from '@reduxjs/toolkit'
const tableSlice = createSlice({
name: 'table',
initialState: { data: [] },
reducers: {
setData: (state, action) => {
state.data = action.payload
},
},
})
export const { setData } = tableSlice.actions
export const store = configureStore({
reducer: {
table: tableSlice.reducer,
},
})
- Connect Redux to React:
Wrap your application with the Provider
component and connect your component to the Redux store.
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { store } from './store'
import { setData } from './store'
import { useTable } from 'react-table'
const columns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function ReduxTableComponent() {
const data = useSelector(state => state.table.data)
const dispatch = useDispatch()
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => dispatch(setData(data)))
}, [dispatch])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
)
}
ReactDOM.render(
<Provider store={store}>
<ReduxTableComponent />
</Provider>,
document.getElementById('root')
)
Explanation of the Example:
- Redux Setup: We create a Redux store and a slice to manage the table data.
- Connecting to Redux: We connect our component to the Redux store using the
useSelector
anduseDispatch
hooks. - Data Fetching: We fetch data from an API and dispatch an action to update the Redux store.
- Rendering: We render the table using the data from the Redux store.
MobX
MobX is another state management library that can be used with React Table. Here’s an example of using MobX for state management:
- Install MobX:
npm install mobx mobx-react-lite
- Set Up MobX:
Create a store.js
file to define the MobX store.
import { observable, action } from 'mobx'
class TableStore {
@observable data = []
@action setData(newData) {
this.data = newData
}
}
export const tableStore = new TableStore()
- Connect MobX to React:
Wrap your application with the Provider
component from mobx-react-lite
.
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'mobx-react-lite'
import { observer } from 'mobx-react-lite'
import { tableStore } from './store'
import { useTable } from 'react-table'
const columns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
const MobXTableComponent = observer(() => {
const [data, setData] = React.useState(tableStore.data)
React.useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
tableStore.setData(data)
setData(data)
})
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
)
})
function App() {
return (
<Provider value={tableStore}>
<MobXTableComponent />
</Provider>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
Explanation of the Example:
- MobX Store: We define a MobX store with an observable
data
array and an actionsetData
. - Observer Component: We use the
observer
decorator to ensure the component re-renders when the MobX store changes. - Data Fetching: We fetch data from an API and update the MobX store with the fetched data.
- Rendering: We render the table using the data from the MobX store.
Server-Side Data Handling
Server-Side Pagination
For large datasets, server-side pagination is often more efficient. Here’s an example of implementing server-side pagination:
import React, { useState, useEffect } from 'react'
import { useTable, usePagination } from 'react-table'
const serverSideColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function ServerSidePaginationComponent() {
const [data, setData] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [page, setPage] = useState(0)
const [pageSize, setPageSize] = useState(10)
useEffect(() => {
setIsLoading(true)
fetch(`https://jsonplaceholder.typicode.com/users?_page=${page + 1}&_limit=${pageSize}`)
.then(response => response.json())
.then(data => {
setData(data)
setIsLoading(false)
})
}, [page, pageSize])
const {
getTableProps,
getTableBodyProps,
headerGroups,
page: serverPage,
prepareRow,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize: currentPageSize },
} = useTable({ columns: serverSideColumns, data: serverPage }, usePagination)
return (
<div>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
<div>
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'First'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'Previous'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'Next'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'Last'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
min={1}
max={pageOptions.length}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
)
}
export default ServerSidePaginationComponent
Explanation of the Example:
- Server-Side Pagination: We fetch data from the API with pagination parameters (
_page
and_limit
). - Pagination Logic: We manage the pagination logic using the
useState
hook anduseEffect
hook. - Pagination Controls: We add custom pagination controls.
Server-Side Filtering and Sorting
Server-side filtering and sorting works similarly to server-side pagination. You can pass the filter and sort parameters to the API and fetch the filtered and sorted data.
Here’s an example:
import React, { useState, useEffect } from 'react'
import { useTable, useFilters, useSortBy, usePagination } from 'react-table'
const serverSideColumns = [
{ Header: 'ID', accessor: 'id', disableFilters: true },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function ServerSideSortingFilteringComponent() {
const [data, setData] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [page, setPage] = useState(0)
const [pageSize, setPageSize] = useState(10)
const [filterText, setFilterText] = useState('')
const [sortText, setSortText] = useState([{ id: 'name', desc: false }])
useEffect(() => {
setIsLoading(true)
fetch(
`https://jsonplaceholder.typicode.com/users?_page=${page + 1}&_limit=${pageSize}&name_like=${filterText}&_sort=${sortText[0].id}&_order=${sortText[0].desc ? 'desc' : 'asc'}`
)
.then(response => response.json())
.then(data => {
setData(data)
setIsLoading(false)
})
}, [page, pageSize, filterText, sortText])
const {
getTableProps,
getTableBodyProps,
headerGroups,
page: serverPage,
prepareRow,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
setGlobalFilter,
state: { pageIndex, pageSize: currentPageSize, globalFilter: currentGlobalFilter },
} = useTable(
{ columns: serverSideColumns, data: serverPage },
useFilters,
useSortBy,
usePagination
)
return (
<div>
<input
value={currentGlobalFilter || ''}
onChange={e => setGlobalFilter(e.target.value)}
style={{ marginBottom: '1 rem' }}
placeholder="Search..."
/>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
<div>
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'First'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'Previous'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'Next'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'Last'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
min={1}
max={pageOptions.length}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
)
}
export default ServerSideSortingFilteringComponent
Explanation of the Example:
- Server-Side Filtering and Sorting: We pass filter and sort parameters to the API to fetch the filtered and sorted data.
- Global Filter: We add a global filter input field to allow users to search the table.
Virtualization
Implementing Virtualization
For large datasets, virtualization can significantly improve performance. React Table supports virtualization out of the box. Here’s an example:
import React, { useState, useEffect } from 'react'
import { useTable, useFilters, useSortBy, usePagination, useRowSelect, useBlockLayout } from 'react-table'
import { FixedSizeList as List } from 'react-window'
function VirtualTableComponent() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
fetch('https://jsonplaceholder.typicode.com/users?_limit=1000')
.then(response => response.json())
.then(data => {
setData(data)
setLoading(false)
})
}, [])
const columns = React.useMemo(
() => [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
],
[]
)
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page, // Instead of using 'rows', we'll use page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize },
} = useTable(
{ columns, data },
useFilters,
useSortBy,
usePagination,
useRowSelect,
useBlockLayout
)
return (
<div>
{loading && <div>Loading...</div>}
<div {...getTableProps()} style={{ height: '400px', width: '600px', overflow: 'auto' }}>
<table
style={{
height: '100%',
width: '100%',
borderCollapse: 'collapse',
}}
>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())} style={{ backgroundColor: 'aliceblue', margin: 0, padding: 0, borderBottom: 'solid 1px black' }}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
<List height={300} itemCount={page.length} itemSize={35} width={550}>
{({ index, style }) => {
prepareRow(page[index])
return (
<tr {...page[index].getRowProps({ style })}>
{page[index].cells.map(cell => (
<td {...cell.getCellProps()} style={{ padding: '10px' }}>
{cell.render('Cell')}
</td>
))}
</tr>
)
}}
</List>
</tbody>
</table>
</div>
<div>
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'First'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'Previous'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'Next'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'Last'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
min={1}
max={pageOptions.length}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
)
}
export default VirtualTableComponent
Explanation of the Example:
- useBlockLayout Hook: We use the
useBlockLayout
hook to enable virtualization. - FixedSizeList Component: We use the
FixedSizeList
component fromreact-window
to render only the visible rows.
Benefits of Virtualization
- Performance: Virtualization improves performance by rendering only the visible parts of the table.
- Scalability: It scales better with large datasets.
Optimizing Performance
Debouncing User Input
Debouncing user input can prevent excessive API requests. Here’s an example of debouncing the global filter:
import React, { useState, useEffect, useCallback } from 'react'
import { useTable, useFilters, useSortBy, usePagination, useRowSelect, useBlockLayout } from 'react-table'
import { FixedSizeList as List } from 'react-window'
import debounce from 'lodash.debounce'
function VirtualTableComponent() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
fetch('https://jsonplaceholder.typicode.com/users?_limit=1000')
.then(response => response.json())
.then(data => {
setData(data)
setLoading(false)
})
}, [])
const columns = React.useMemo(
() => [
{ Header: 'ID', accessor: 'id', disableSortBy: true, disableFilters: true },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
],
[]
)
const [globalFilter, setGlobalFilter] = useState('')
const debounceGlobalFilter = useCallback(debounce(value => setGlobalFilter(value), 500), [])
useEffect(() => {
debounceGlobalFilter(globalFilter)
}, [globalFilter])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
preGlobalFilteredRows,
setGlobalFilter,
} = useTable(
{ columns, data, initialState: { pageIndex: 0 } },
useFilters,
useSortBy,
usePagination,
useRowSelect,
useBlockLayout
)
return (
<div>
{loading && <div>Loading...</div>}
<div>
<input
value={globalFilter || ''}
onChange={e => setGlobalFilter(e.target.value)}
style={{ width: '100%', marginBottom: '1rem' }}
placeholder="Search..."
/>
</div>
<div {...getTableProps()} style={{ height: '400px', width: '600px', overflow: 'auto' }}>
<table
style={{
height: '100%',
width: '100%',
borderCollapse: 'collapse',
}}
>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())} style={{ backgroundColor: 'aliceblue', margin: 0, padding: 0, borderBottom: 'solid 1px black' }}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()} style={{ padding: '10px' }}>
{cell.render('Cell')}
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
<div>
<button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
{'First'}
</button>{' '}
<button onClick={() => previousPage()} disabled={!canPreviousPage}>
{'Previous'}
</button>{' '}
<button onClick={() => nextPage()} disabled={!canNextPage}>
{'Next'}
</button>{' '}
<button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
{'Last'}
</button>{' '}
<span>
Page{' '}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>{' '}
</span>
<span>
| Go to page:{' '}
<input
type="number"
defaultValue={pageIndex + 1}
onChange={e => {
const page = e.target.value ? Number(e.target.value) - 1 : 0
gotoPage(page)
}}
style={{ width: '100px' }}
min={1}
max={pageOptions.length}
/>
</span>{' '}
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value))
}}
>
{[10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
</div>
)
}
export default ServerSideSortingFilteringComponent
Explanation of the Example:
- Debouncing: We use the
lodash.debounce
function to debounce the global filter input.
Memoizing Components
Memoizing components can improve performance by preventing unnecessary re-renders. Here’s an example of memoizing a table row:
const MemoizedRow = React.memo(({ row, prepareRow }) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()} style={{ padding: '5px', border: '1px solid gray' }}>
{cell.render('Cell')}
</td>
))}
</tr>
)
})
function OptimizedTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
} = useTable({ columns: paginatedColumns, data }, usePagination)
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())} style={{ padding: '5px', border: '1px solid gray' }}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => (
<MemoizedRow key={row.id} row={row} prepareRow={prepareRow} />
))}
</tbody>
</table>
)
}
export default OptimizedTableComponent
Explanation of the Example:
- React.memo: We use
React.memo
to memoize theMemoizedRow
component.
Using React.memo
React.memo
is a higher-order component that memoizes the component so it only re-renders when its props change.
Here’s an example:
const MemoizedRow = React.memo(({ row, prepareRow }) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()} style={{ padding: '5px', border: '1px solid gray' }}>
{cell.render('Cell')}
</td>
))}
</tr>
)
})
Explanation of the Example:
- React.memo: We wrap the
Row
component withReact.memo
to memoize it.
Best Practices
Error Handling
Handling API Errors
Error handling is crucial when fetching data from an API. Here’s an example of handling API errors:
import React, { useState, useEffect } from 'react'
import { useTable } from 'react-table'
const errorColumns = [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
]
function ErrorTableComponent() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch('https://jsonplaceholder.typicode.com/user') // intentional typo to cause error
.then(response => response.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(error => {
setError(error)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns: errorColumns, data })
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
)
}
export default ErrorTableComponent
Explanation of the Example:
- Error Handling: We handle API errors using
catch
and update theerror
state with the error message.
Displaying Fallback Content
When data is loading or an error occurs, you can display fallback content to improve user experience.
Here’s an example:
<div>
{loading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
{data.length === 0 && !loading && <div>No data</div>}
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())} style={{ padding: '5px', border: '1px solid black' }}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()} style={{ padding: '5px', border: '1px solid gray' }}>
{cell.render('Cell')}
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
Explanation of the Example:
- Fallback Content: We display loading, error, and no data messages.
- Conditional Rendering: We conditionally render the table or fallback content based on the state.
Accessibility Considerations
Keyboard Navigation
Keyboard navigation is important for accessibility. React Table provides default keyboard navigation, but you can customize it further.
Here’s an example:
<table {...getTableProps()} tabIndex="0">
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())} style={{ padding: '5px', border: '1px solid black' }}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()} style={{ padding: '5px', border: '1px solid gray' }}>
{cell.render('Cell')}
</td>
))}
</tr>
)
})}
</tbody>
</table>
Explanation of the Example:
- TabIndex: We add
tabIndex="0"
to the table to make it focusable. - getSortByToggleProps: This property enables keyboard navigation for sorting.
Screen Reader Compatibility
Screen readers are essential for users with visual impairments. React Table provides default screen reader support, but you can enhance it.
Here’s an example:
<table {...getTableProps()} aria-label="Data Table">
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())} aria-sort={column.isSorted ? (column.isSortedDesc ? 'descending' : 'ascending') : undefined}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
Explanation of the Example:
- aria-label: We add an
aria-label
to the table. - aria-sort: We add
aria-sort
attributes to the headers to indicate sorting.
Code Optimization
Refactoring for Readability
Refactoring your code helps improve readability and maintainability. Here’s an example of refactoring a large table component:
const TableHeader = ({ headerGroups }) => (
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
)
const TableBody = ({ page, prepareRow }) => (
<tbody>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
)
function OptimizedTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const columns = React.useMemo(
() => [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
],
[]
)
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
} = useTable({ columns, data }, usePagination)
return (
<table {...getTableProps()}>
<TableHeader headerGroups={headerGroups} />
<TableBody page={page} prepareRow={prepareRow} />
</table>
)
}
export default OptimizedTableComponent
Explanation of the Example:
- Refactoring: We refactor the table header and body into separate
TableHeader
andTableBody
components.
Modular Code Design
Modular code design improves maintainability and readability. Here’s an example:
// TableHeader.jsx
import React from 'react'
const TableHeader = ({ headerGroups }) => (
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
)
export default TableHeader
// TableBody.jsx
import React from 'react'
const TableBody = ({ page, prepareRow }) => (
<tbody>
{page.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
)
export default TableBody
// OptimizedTableComponent.jsx
import React, { useState, useEffect } from 'react'
import { useTable } from 'react-table'
import TableHeader from './TableHeader'
import TableBody from './TableBody'
function OptimizedTableComponent() {
const [data, setData] = useState([])
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => setData(data))
}, [])
const columns = React.useMemo(
() => [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
],
[]
)
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns, data }, usePagination)
return (
<table {...getTableProps()} style={{ border: '1px solid black' }}>
<TableHeader headerGroups={headerGroups} />
<TableBody page={rows} prepareRow={prepareRow} />
</table>
)
}
export default OptimizedTableComponent
Explanation of the Example:
- Modular Components: We break down the table into smaller components for better organization and reusability.
Troubleshooting Common Issues
Debugging Tips
Console Logging
Console logging is a basic but powerful debugging technique. Here’s an example:
const columns = React.useMemo(
() => [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
],
[]
)
function DebuggingTableComponent() {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(error => {
setError(error)
setLoading(false)
})
}, [])
console.log('Data:', data)
console.log('Loading:', loading)
console.log('Error:', error)
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns, data })
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()}>{cell.render('Cell')}</td>
))}
</tr>
)
})}
</tbody>
</table>
)
}
export default DebuggingTableComponent
Explanation of the Example:
- Console Logging: We log the data, loading, and error states to the console for debugging.
Using React Developer Tools
React Developer Tools is a powerful tool for debugging React applications. It allows you to inspect the component tree, props, and state.
Common Errors
Data Fetch Errors
Data fetch errors often occur due to network issues or incorrect API endpoints. Here’s how to handle such errors:
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(error => {
setError(error)
setLoading(false)
})
}, [])
Explanation of the Example:
- Error Handling: We handle errors using the
catch
method of the fetch API.
Render Issues
Render issues can occur due to incorrect table configurations or state management. Here’s an example of debugging a render issue:
const columns = React.useMemo(
() => [
{ Header: 'ID', accessor: 'id' },
{ Header: 'Name', accessor: 'name' },
{ Header: 'Email', accessor: 'email' },
],
[]
)
const RenderTableComponent = () => {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(error => {
setError(error)
setLoading(false)
})
}, [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = useTable({ columns, data })
return (
<div>
{loading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
<table {...getTableProps()} style={{ border: '1px solid black' }}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<th {...column.getHeaderProps(column.getSortByToggleProps())} style={{ padding: '5px', border: '1px solid black' }}>
{column.render('Header')}
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map(row => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => (
<td {...cell.getCellProps()} style={{ padding: '5px', border: '1px solid gray' }}>
{cell.render('Cell')}
</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
export default RenderTableComponent
Explanation of the Example:
- Conditional Rendering: We conditionally render loading, error, and table content based on the state.
Additional Resources
Official Documentation
React Table Documentation
- You can find the official React Table documentation here.
- The documentation is comprehensive and includes numerous examples and tutorials.
Examples and Tutorials
- The official documentation includes a variety of examples and tutorials to help you get started.
Community Resources
Forums and Q&A Sites
- You can ask questions and find answers on forums like Stack Overflow.
GitHub Repositories
- You can explore community-built examples and templates on GitHub:
By following this comprehensive guide, you will have a solid understanding of how to use React Table for data management in your React applications. From setting up a basic table to handling complex data flows and optimizing performance, this guide covers everything you need to build efficient and user-friendly data tables with React Table.