I'm using react-table to include the rows and pagination in the table as shown in this example: https://react-table.tanstack.com/docs/examples/row-selection-and-pagination.
I've done my example as the tutorial says, but it doesn't function as intended. I can only select 1 row before it stops updating the selectedRowsId. Deselecting the row doesn't clear it either. What am I doing wrong?
import React, { useMemo, forwardRef, useRef, useEffect } from "react";
import {
useTable,
useSortBy,
useFilters,
useGlobalFilter,
usePagination,
useRowSelect
} from "react-table";
import matchSorter from "match-sorter";
import '../../static/scss/table.scss'
function DefaultColumnFilter({
column: { filterValue, preFilteredRows, setFilter },
}) {
const count = preFilteredRows.length;
return (
<input
value={filterValue || ""}
onChange={(e) => {
setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely
}}
placeholder={`Search ${count} records...`}
/>
);
}
function SelectColumnFilter({
column: { filterValue, setFilter, preFilteredRows, id },
}) {
// Calculate the options for filtering
// using the preFilteredRows
const options = React.useMemo(() => {
const options = new Set();
preFilteredRows.forEach((row) => {
options.add(row.values[id]);
});
return [...options.values()];
}, [id, preFilteredRows]);
// Render a multi-select box
return (
<select
value={filterValue}
onChange={(e) => {
setFilter(e.target.value || undefined);
}}
>
<option value="">All</option>
{options.map((option, i) => (
<option key={i} value={option}>
{option}
</option>
))}
</select>
);
}
function fuzzyTextFilterFn(rows, id, filterValue) {
return matchSorter(rows, filterValue, { keys: [(row) => row.values[id]] });
}
fuzzyTextFilterFn.autoRemove = (val) => !val;
const IndeterminateCheckbox = forwardRef(
({ indeterminate, ...rest }, ref) => {
const defaultRef = useRef()
const resolvedRef = ref || defaultRef
useEffect(() => {
resolvedRef.current.indeterminate = indeterminate
}, [resolvedRef, indeterminate])
return (
<>
<input type="checkbox" ref={resolvedRef} {...rest} />
</>
)
}
)
export default function Table({ data }) {
console.log(data[0].title);
const filterTypes = useMemo(
() => ({
// Add a new fuzzyTextFilterFn filter type.
fuzzyText: fuzzyTextFilterFn,
// Or, override the default text filter to use
// "startWith"
text: (rows, id, filterValue) => {
return rows.filter((row) => {
const rowValue = row.values[id];
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.startsWith(String(filterValue).toLowerCase())
: true;
});
},
}),
[]
);
const defaultColumn = useMemo(
() => ({
// Let's set up our default Filter UI
Filter: DefaultColumnFilter,
}),
[]
);
const columns = useMemo(
() => [
{
Header: "Naziv",
accessor: "title",
},
{
Header: "Tip",
accessor: "activity_type_id",
Filter: SelectColumnFilter,
filter: "includes",
},
{
Header: "Datum",
accessor: "start_time",
},
{
Header: "Mjesto",
accessor: "location",
},
{
Header: "Organizator",
accessor: "team_id",
Filter: SelectColumnFilter,
filter: "includes",
},
{
Header: "Odgovorna osoba",
accessor: "user_id",
},
],
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
selectedFlatRows,
state: { pageIndex, pageSize, selectedRowIds },
} = useTable(
{
columns,
data,
defaultColumn,
filterTypes,
initialState: { pageIndex: 0 },
},
useFilters,
useGlobalFilter,
useSortBy,
usePagination,
useRowSelect,
hooks => {
hooks.visibleColumns.push(columns => [
// Let's make a column for selection
{
id: 'selection',
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox
Header: ({ getToggleAllPageRowsSelectedProps }) => (
<div>
<IndeterminateCheckbox {...getToggleAllPageRowsSelectedProps()} />
</div>
),
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: ({ row }) => (
<div>
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
</div>
),
},
...columns,
])
}
);
return (
<div>
<legend className="legend">Popis aktivnosti</legend>
<>
<pre>
<code>
{JSON.stringify(
{
pageIndex,
pageSize,
pageCount,
canNextPage,
canPreviousPage,
},
null,
2
)}
</code>
</pre>
<table {...getTableProps()}>
<thead>
{
// Loop over the header rows
headerGroups.map((headerGroup) => (
// Apply the header row props
<tr {...headerGroup.getHeaderGroupProps()}>
{
// Loop over the headers in each row
headerGroup.headers.map((column) => (
// Apply the header cell props
<th
{...column.getHeaderProps(
column.getSortByToggleProps()
)}
>
{
// Render the header
column.render("Header")
}
<span>
{column.isSorted
? column.isSortedDesc
? " 🔽"
: " 🔼"
: ""}
</span>
</th>
))
}
{}
<th>Nige</th>
<th>Nei</th>
</tr>
))
}
{
// Loop over the header rows
headerGroups.map((headerGroup) => (
// Apply the header row props
<tr {...headerGroup.getHeaderGroupProps()}>
{
// Loop over the headers in each row
headerGroup.headers.map((column) => (
// Apply the header cell props
<th>
<div>
{column.canFilter ? column.render("Filter") : null}
</div>
</th>
))
}
{}
<th></th>
<th></th>
</tr>
))
}
</thead>
{/* Apply the table body props */}
<tbody {...getTableBodyProps()}>
{page.map((row, i) => {
prepareRow(row)
return (
<tr {...row.getRowProps()}>
{row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>
})}
</tr>
)
})}
</tbody>
</table>
<div className="pagination">
<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>
<p>Selected Rows: {Object.keys(selectedRowIds).length}</p>
<pre>
<code>
{JSON.stringify(
{
selectedRowIds: selectedRowIds,
'selectedFlatRows[].original': selectedFlatRows.map(
d => d.original
),
},
null,
2
)}
</code>
</pre>
</>
</div>
);
}
I found the solution. index.js file has the <React.StrictMode> wrapping the <App />. Removing the <React.StrictMode> fixes it and functions properly. This is probably a bug and should be fixed.
Related
I am getting a list of Products from an API call and I am rendering it through react-table.
const getItemsList=()=>{
getAuthorization()
.get("product/all_products/")
.then((res) => {
setProducts(res.data);
dispatch(setAPIDetailsItemsTable(res.data));
})
.catch((err) => {
console.log("Error in getting products", err);
});
}
useEffect(() => {
// Get all products
getItemsList();
}, []);
I am using redux for state Management
const data = APIDetailsItemsTable
const columns = React.useMemo(
() => [
{
Header: "Number",
accessor: "product_id",
},
{
Header: "Item Name",
accessor: "name",
},
{
Header: "Item Type",
accessor: "product_type",
},
{
Header: "Status",
accessor: "status",
Cell: StatusPill
}
],
[]
);
return (
< AddGuideItemstable
columns={columns}
data={data == undefined ? [] : data}
text="Undefined" />
)
/>
Here is the table component. I am working on Editing part. The checkbox can select only one item from the list.
function AddGuideItemstable({ columns, data, text }) {
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
visibleColumns,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state,
preGlobalFilteredRows,
setGlobalFilter,
selectedFlatRows,
state: { selectedRowIds }
} = useTable(
{
columns,
data,
initialState: {selectedRowIds},
stateReducer: (newState, action) => {
if (action.type === "toggleRowSelected") {
newState.selectedRowIds = {
[action.id]: true,
}
}
return newState;
},
},
useFilters, // useFilters!
useGlobalFilter,
useSortBy,
usePagination, // new
useRowSelect,
(hooks) => {
hooks.visibleColumns.push((columns) => {
return [
...columns,
{
Header: "Choose Items",
id: "selection",
Cell: ({ row }) => (
<div className="flex flex-col ml-6">
<CheckBox {...row.getToggleRowSelectedProps()}/>
</div>
),
//// dispatching selected item ID if changed
useEffect(() => {
let Id = selectedFlatRows.map(
d => d.original.id)
dispatch(setAddGuideItemID(Id))
console.log("selected row id", Id)
}, [selectedRowIds])
// Render the UI for your table
return (
<>
<table
{...getTableProps()}
className="min-w-full bg-transparent divide-y divide-gray-200"
>
<thead className=" border-b-8 border-white">
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
// Add the sorting props to control sorting. For this example
// we can add them into the header props
<th
scope="col"
className="group px-2 py-3 text-center text-sm font-medium text-gray-400 font-Roboto tracking-wider"
{...column.getHeaderProps(
column.getSortByToggleProps()
)}
>
<div className="flex items-center justify-between">
{column.render("Header")}
{/* Add a sort direction indicator */}
<span>
{column.isSorted ? (
column.isSortedDesc ? (
<SortDownIcon className="w-4 h-4 text-gray-100" />
) : (
<SortUpIcon className="w-4 h-4 text-gray-100" />
)
) : (
<SortIcon className="w-4 h-4 text-gray-100 opacity-0 group-hover:opacity-100" />
)}
</span>
</div>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()} className="bg-white">
{page.map((row, index) => {
// new
prepareRow(row);
return (
<tr
{...row.getRowProps()}
className={
index % 2 === 0
? "bg-cyan-100 border-b-8 border-white"
: "bg-white border-b-8 border-white"
}
>
{row.cells.map((cell) => {
return (
<td
{...cell.getCellProps()}
className="pr-1 pl-2 whitespace-nowrap"
role="cell"
>
{cell.column.Cell.name === "defaultRenderer" ? (
<div className="text-sm font-semibold text-black">
{cell.render("Cell")}
</div>
) : (
cell.render("Cell")
)}
</td>
);
})}
</tr>
);
})}
</tbody>
The checkbox component is as below
import react, { forwardRef, useEffect, useRef, useState } from "react";
export const CheckBox = forwardRef(({ indeterminate, ...rest }, ref) => {
const defaultRef = useRef();
const resolvedRef = ref || defaultRef;
useEffect(() => {
resolvedRef.current.indeterminate = indeterminate;
}, [resolvedRef, indeterminate]);
return (
<>
<div class="flex items-center">
<input
type="checkbox"
ref={resolvedRef}
{...rest}
id="A3-yes"
name="A3-confirmation"
class="opacity-0 absolute h-8 w-8"
/>
</>
);
});
I have an ID stored in a state
let ID = useSelector((state)=> state.guide.AddGuideResID)
What I want to do is if " ID " in the state is equal to the "ID in the products list" then mark the checkbox checked for that row.
I want to achieve like this. I want to mark only single item thats original ID matches the ID in my redux state. Checkbox checked manually
I have a slightly modified implementation of the react-table v8 filters: https://tanstack.com/table/v8/docs/examples/react/filters
In the original they seem to use a custom filter function to filter the table globally but it requires another library that i don't want to include, the documentation isn't very clear about this but there seems to be built-in functions that i can use: https://tanstack.com/table/v8/docs/api/features/filters
However the table stops filtering as soon as i change it, i tried not including the globalFilterFn as well as setting it to globalFilterFn: "includesString" which is one of the built-in functions i mentioned but nothing has worked so far.
here is my code:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import {
useReactTable,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
FilterFn,
ColumnDef,
flexRender
} from "#tanstack/react-table";
//import { RankingInfo, rankItem } from "#tanstack/match-sorter-utils";
import { makeData, Person } from "./makeData";
/* declare module "#tanstack/table-core" {
interface FilterMeta {
itemRank: RankingInfo;
}
}
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
// Rank the item
const itemRank = rankItem(row.getValue(columnId), value);
// Store the itemRank info
addMeta({
itemRank
});
// Return if the item should be filtered in/out
return itemRank.passed;
}; */
function App() {
const rerender = React.useReducer(() => ({}), {})[1];
const [globalFilter, setGlobalFilter] = React.useState("");
const columns = React.useMemo<ColumnDef<Person>[]>(
() => [
{
header: "Name",
footer: (props) => props.column.id,
columns: [
{
accessorKey: "firstName",
cell: (info) => info.getValue(),
footer: (props) => props.column.id
},
{
accessorFn: (row) => row.lastName,
id: "lastName",
cell: (info) => info.getValue(),
header: () => <span>Last Name</span>,
footer: (props) => props.column.id
},
{
accessorFn: (row) => `${row.firstName} ${row.lastName}`,
id: "fullName",
header: "Full Name",
cell: (info) => info.getValue(),
footer: (props) => props.column.id
}
]
},
{
header: "Info",
footer: (props) => props.column.id,
columns: [
{
accessorKey: "age",
header: () => "Age",
footer: (props) => props.column.id
},
{
header: "More Info",
columns: [
{
accessorKey: "visits",
header: () => <span>Visits</span>,
footer: (props) => props.column.id
},
{
accessorKey: "status",
header: "Status",
footer: (props) => props.column.id
},
{
accessorKey: "progress",
header: "Profile Progress",
footer: (props) => props.column.id
}
]
}
]
}
],
[]
);
const [data, setData] = React.useState(() => makeData(500));
const refreshData = () => setData((old) => makeData(500));
const table = useReactTable({
data,
columns,
state: {
globalFilter
},
onGlobalFilterChange: setGlobalFilter,
//globalFilterFn: "includesString",
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel()
});
return (
<div className="p-2">
<div>
<input
value={globalFilter ?? ""}
onChange={(event) => setGlobalFilter(event.target.value)}
className="p-2 font-lg shadow border border-block"
placeholder="Search all columns..."
/>
</div>
<div className="h-2" />
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : (
<>
<div
{...{
className: header.column.getCanSort()
? "cursor-pointer select-none"
: "",
onClick: header.column.getToggleSortingHandler()
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: " 🔼",
desc: " 🔽"
}[header.column.getIsSorted() as string] ?? null}
</div>
</>
)}
</th>
);
})}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => {
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
<div className="h-2" />
<div className="flex items-center gap-2">
<button
className="border rounded p-1"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
{"<<"}
</button>
<button
className="border rounded p-1"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{"<"}
</button>
<button
className="border rounded p-1"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{">"}
</button>
<button
className="border rounded p-1"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
{">>"}
</button>
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</strong>
</span>
<span className="flex items-center gap-1">
| Go to page:
<input
type="number"
defaultValue={table.getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
table.setPageIndex(page);
}}
className="border p-1 rounded w-16"
/>
</span>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
<div>{table.getPrePaginationRowModel().rows.length} Rows</div>
<div>
<button onClick={() => rerender()}>Force Rerender</button>
</div>
<div>
<button onClick={() => refreshData()}>Refresh Data</button>
</div>
<pre>{JSON.stringify(table.getState(), null, 2)}</pre>
</div>
);
}
const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Failed to find the root element");
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
a link to CodeSanbox with it: https://codesandbox.io/s/long-monad-652jcm?file=/src/main.tsx
I'm still very inexperienced in React and even more with typescript so maybe i'm missing something obvious.
am i misinterpreting the docs, maybe the custom function is necessary?
There is a current issue on GitHub where you can't use a global filter if one of your column is using number fields. You can simply solve it by replacing the number by a string in your data. (.toString(), and sorting still works). If worked fine for me afterwards. :)
Your globalFilter doesn't do anything, you need to test if the contents of the table are equal to the filter or not.
I am trying to expand a react table 7 automatically on table load. If I hard code the tables expanded initialState it works, but I need to be able to do it programmatically since the number of rows being loaded changes depending on other data selection factors.
I've setup my table so that it takes in 2 props, expandedRows which is a boolean and expandedRowObj which is an object that contains the index of each row and a true value to be expanded.
I'm using useEffect to loop through the data and create a new object that has the data index as a key and sets true as the property. I then pass this array of objects as a prop to the tables initialState.
I can see using the devTools that the intitalState on the table is being set to:
initialState: {
expanded: [{0: true}, {1: true}, {2: true},{3: true}]
}
however, rows are not being expanded.
If I do not use the useEffect function to set the expandedRows state and just hardcode a variable called expandedRows the table expands as expected. I'm guessing that there is a disconnect between when the table renders and the initial state is set but I'm not sure.
Here is a sandbox to demo the issue: https://codesandbox.io/s/dazzling-tdd-x4890?file=/src/App.js
For those who do not want to click on links, heres all the relevant code:
TABLE
import {
useTable,
useSortBy,
useGlobalFilter,
useFilters,
useResizeColumns,
useFlexLayout,
useExpanded,
usePagination
} from "react-table";
import {
Table,
InputGroup,
FormControl,
Row,
Col,
Button
} from "react-bootstrap";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
import {
faArrowDown,
faArrowUp,
faAngleDoubleLeft,
faAngleDoubleRight,
faAngleLeft,
faAngleRight
} from "#fortawesome/free-solid-svg-icons";
import GlobalFilter from "./GlobalFilter";
import ColumnFilter from "./ColumnFilter";
import "./Table.css";
import "bootstrap/dist/css/bootstrap.min.css";
const MyTable = ({
columns: userColumns,
data,
renderRowSubComponent,
rowOnClick,
rowClickHandler,
headerColor,
showPagination,
showGlobalFilter,
expandRows,
expandedRowObj
}) => {
const filterTypes = React.useMemo(
() => ({
includes: (rows, id, filterValue) => {
return rows.filter((row) => {
const rowValue = row.values[id];
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.includes(String(filterValue).toLowerCase())
: true;
});
},
startsWith: (rows, id, filterValue) => {
return rows.filter((row) => {
const rowValue = row.values[id];
return rowValue !== undefined
? String(rowValue)
.toLowerCase()
.startsWith(String(filterValue).toLowerCase())
: true;
});
}
}),
[]
);
const sortTypes = React.useMemo(
() => ({
dateSort: (a, b) => {
a = new Date(a).getTime();
b = new Date(b).getTime();
return b > a ? 1 : -1;
}
}),
[]
);
const defaultColumn = React.useMemo(
() => ({
Filter: ColumnFilter,
disableFilters: true,
minWidth: 30,
width: 150,
maxWidth: 500
}),
[]
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
setGlobalFilter,
state: { globalFilter, pageIndex, pageSize }
} = useTable(
{
columns: userColumns,
data,
initialState: {
expanded:
expandRows && expandedRowObj.hasOwnProperty(0) ? expandedRowObj : {}
},
defaultColumn,
filterTypes,
sortTypes
},
useGlobalFilter,
useFilters,
useSortBy,
useResizeColumns,
useExpanded,
usePagination,
useFlexLayout
);
return (
<React.Fragment>
<Row className="float-right">
<Col>
{showGlobalFilter ? (
<GlobalFilter filter={globalFilter} setFilter={setGlobalFilter} />
) : (
""
)}
</Col>
</Row>
<Row>
<Col>
<Table
striped
bordered
hover
size="sm"
responsive
{...getTableProps()}
>
<thead>
{headerGroups.map((headerGroup, i) => (
<React.Fragment key={headerGroup.headers.length + "_hfrag"}>
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map((column) => (
<th
key={column.id}
className={`p-2 table-header ${
headerColor ? "primary-" + headerColor : "primary-deq"
}`}
{...column.getHeaderProps()}
>
<span {...column.getSortByToggleProps()}>
{column.render("Header")}
{column.isSorted ? (
column.isSortedDesc ? (
<FontAwesomeIcon
className="ms-3"
icon={faArrowDown}
/>
) : (
<FontAwesomeIcon
className="ms-3"
icon={faArrowUp}
/>
)
) : (
""
)}
</span>
<div
{...column.getResizerProps()}
className="resizer"
/>
{column.canResize && (
<div
{...column.getResizerProps()}
className={`resizer ${
column.isResizing ? "isResizing" : ""
}`}
/>
)}
<div>
{column.canFilter ? column.render("Filter") : null}
</div>
</th>
))}
</tr>
</React.Fragment>
))}
</thead>
<tbody {...getTableBodyProps()}>
{page.map((row, i) => {
prepareRow(row);
return (
<React.Fragment key={i + "_frag"}>
<tr
{...row.getRowProps()}
onClick={
rowOnClick
? () => rowClickHandler(row.original)
: () => ""
}
>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()}>
{cell.render("Cell")}
</td>
);
})}
</tr>
{row.isExpanded ? (
<tr>
<td>
<span className="subTable">
{renderRowSubComponent({ row })}
</span>
</td>
</tr>
) : null}
</React.Fragment>
);
})}
</tbody>
</Table>
{showPagination ? (
<Row className="mt-2 text-center">
<Col>
<Button
className="me-2"
size="sm"
variant="secondary"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
>
<FontAwesomeIcon icon={faAngleDoubleLeft} />
</Button>
<Button
className="me-2"
size="sm"
variant="secondary"
onClick={() => previousPage()}
disabled={!canPreviousPage}
>
<FontAwesomeIcon icon={faAngleLeft} />
</Button>
</Col>
<Col>
<span>
Page{" "}
<strong>
{pageIndex + 1} of {pageOptions.length}
</strong>
</span>
<span>
| Go to page:{" "}
<InputGroup
size="sm"
style={{ width: "20%", display: "inline-flex" }}
>
<FormControl
type="number"
defaultValue={pageIndex + 1}
onChange={(e) => {
const page = e.target.value
? Number(e.target.value) - 1
: 0;
gotoPage(page);
}}
/>
</InputGroup>
</span>
<InputGroup
size="sm"
style={{ width: "30%", display: "inline-flex" }}
>
<FormControl
className="mt-4"
size="sm"
as="select"
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
}}
>
{[5, 10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</FormControl>
</InputGroup>
</Col>
<Col>
<Button
className="me-2"
size="sm"
variant="secondary"
onClick={() => nextPage()}
disabled={!canNextPage}
>
<FontAwesomeIcon icon={faAngleRight} />
</Button>
<Button
className="me-2"
size="sm"
variant="secondary"
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
>
<FontAwesomeIcon icon={faAngleDoubleRight} />
</Button>
</Col>
</Row>
) : (
""
)}
</Col>
</Row>
</React.Fragment>
);
};
MyTable.defaultProps = {
rowOnClick: false,
showPagination: false,
expandRows: false,
expandedRowObj: {}
};
MyTable.propTypes = {
/** Specified if pagination should show or not */
showPagination: PropTypes.bool.isRequired,
/** Specifies if there should be a row onClick action*/
rowOnClick: PropTypes.bool.isRequired,
/** OPTIONAL: The onClick Action to be taken */
rowClickHandler: PropTypes.func,
/** header color background. There are six possible choices. Refer to ReadMe file for specifics */
headerColor: PropTypes.string
};
USING TABLE COMPONENT
const GroupedSamplingStationTable = (props) => {
const [expandedRows, setExpandedRows] = useState();
//const expandedRows = [{ 0: true }, { 1: true }, { 2: true }, { 3: true }]; //This works
const columns = [
{
Header: () => null,
id: "expander",
width: 30,
Cell: ({ row }) => (
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? (
<FontAwesomeIcon className="font-icon" icon={faCaretDown} />
) : (
<FontAwesomeIcon className="font-icon" icon={faCaretRight} />
)}
</span>
)
},
{
Header: "Sample Group ID",
accessor: "groupId",
width: 75
},
{
Header: "Sample Group",
accessor: "groupName",
width: 200
}
];
const details = React.useMemo(
() => [
{
Header: "Source ID",
accessor: "sourceId",
width: 50
},
{
Header: "Source Name",
accessor: "sourceName",
width: 125
},
{
Header: "Sample Group Details",
accessor: "groupDetails",
width: 100
},
{
Header: "System",
accessor: (d) => {
return d.systemNumber + " " + d.systemName;
},
width: 200
}
],
[]
);
const subTable = React.useCallback(
({ row }) =>
row.original.groupDetails.length > 0 ? (
<MyTable
columns={details}
data={row.original.groupDetails}
headerColor="grey"
/>
) : (
"No Data"
),
[details]
);
useEffect(() => {
if (data) {
let array = [];
if (data.data.getGroupedSamplingStationBySystemId.length > 0) {
data.data.getGroupedSamplingStationBySystemId.forEach((elem, index) => {
let obj = {};
obj[index] = true;
array.push(obj);
});
} else {
let obj = {};
obj[0] = false;
}
setExpandedRows(array);
}
}, []);
return (
<>
{data.data.getGroupedSamplingStationBySystemId.length > 0 ? (
<MyTable
data={data.data.getGroupedSamplingStationBySystemId}
columns={columns}
renderRowSubComponent={subTable}
expandRows={true}
expandedRowObj={expandedRows}
/>
) : (
<span>
<em>No data was found for grouped sampling stations.</em>
</span>
)}
</>
);
};
DATA EXAMPLE
data = {
data: {
getGroupedSamplingStationBySystemId: [
{
systemId: 1289,
groupId: "8053",
groupName: "S28-UTAH18026UTAH18103",
groupDetails: [
{
sourceId: "WS005",
sourceName: "MT OLYMPUS SPRING ABND",
groupDetails: " ",
systemNumber: "UTAH18026",
systemName: "SALT LAKE CITY WATER SYSTEM"
},
{
sourceId: "WS001",
sourceName: "MT OLYMPUS SPRING",
groupDetails: " ",
systemNumber: "UTAH18103",
systemName: "MOUNT OLYMPUS WATERS"
}
]
},
{
systemId: 1289,
groupId: "8085",
groupName: "S29-UTAH18026UTAH18050",
groupDetails: [
{
sourceId: "WS007",
sourceName: "LOWER BOUNDARY SPRING TSFR",
groupDetails: " ",
systemNumber: "UTAH18026",
systemName: "SALT LAKE CITY WATER SYSTEM"
},
{
sourceId: "WS001",
sourceName: "LOWER BOUNDARY SPRING",
groupDetails: " ",
systemNumber: "UTAH18050",
systemName: "BOUNDARY SPRING WATER CO"
}
]
},
{
systemId: 1289,
groupId: "8193",
groupName: "S30-UTAH18026UTAH18028",
groupDetails: [
{
sourceId: "WS039",
sourceName: "RICHARDS DITCH WELL [DISCONNECTED]",
groupDetails: "IGNORE THIS ONE",
systemNumber: "UTAH18026",
systemName: "SALT LAKE CITY WATER SYSTEM"
},
{
sourceId: "WS027",
sourceName: "RICHARDS DITCH WELL (SOLD/TRANSFERRED)",
groupDetails: " ",
systemNumber: "UTAH18028",
systemName: "SANDY CITY WATER SYSTEM"
}
]
},
{
systemId: 1289,
groupId: "7956",
groupName: "S63-UTAH18026UTAH18028",
groupDetails: [
{
sourceId: "WS031",
sourceName: "7901 S HIGHLAND WELL TSFR",
groupDetails: " ",
systemNumber: "UTAH18026",
systemName: "SALT LAKE CITY WATER SYSTEM"
},
{
sourceId: "WS026",
sourceName: "LITTLE COTTONWOOD WELL",
groupDetails: " ",
systemNumber: "UTAH18028",
systemName: "SANDY CITY WATER SYSTEM"
}
]
}
]
}
};
Using a memoized array, instead of a state array mutated by useEffect, seems to work just fine (sandbox):
const expandedRows = React.useMemo(() => {
if (data?.data) {
let arr = [{0: false}];
let d = data.data;
if (d.getGroupedSamplingStationBySystemId.length > 0) {
arr = d.getGroupedSamplingStationBySystemId.map((sid, ind) => {
return { [ind]: true };
});
}
return arr;
}
}, []);
I am new to REACT and having trouble getting a table component to work correctly.
I am using the react-tables package and the tutorial example here but I am doing everything in a table.js component file and adding it to my App.js. Basically, I am trying to create a table with pagination, sorting, and selectable rows with checkboxes. My issue is that I am only getting the checkboxes to populate in the header (see below).
Here is my code; what am I doing incorrectly here? Note - my rows are being created using page, not row and are coming from my API. Any suggestions on how to fix this?
import React, { useMemo, useEffect, useState } from 'react'
import reactTable, {useTable, useRowSelect, usePagination, useSortBy} from 'react-table'
import axios from "axios";
import { COLUMNS } from './columns'
import './Table.css'
export const Table = () => {
const [loadingData, setLoadingData] = useState(true);
const columns = useMemo(() => COLUMNS, []);
const [data, setData] = useState([]);
const IndeterminateCheckbox = React.forwardRef(
//This is the function for the checkboxes in page select
({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef()
const resolvedRef = ref || defaultRef
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate
}, [resolvedRef, indeterminate])
return (
<>
<input type="checkbox" ref={resolvedRef} {...rest} />
</>
)
}
)
useEffect(() => {
async function getData() {
await axios
.get("http://localhost:5000/my/api")
.then((response) => {
// check if the data is populated
console.log(response.data);
setData(response.data);
// you tell it that you had the result
setLoadingData(false);
});
}
if (loadingData) {
// if the result is not ready so you make the axios call
getData();
}
}, []);
const tableInstance = useTable({
columns,
data,
initialState: { pageIndex: 2 }
}, useSortBy,
usePagination,
useRowSelect,
hooks => {
hooks.visibleColumns.push(columns => [
// Let's make a column for selection
{
id: 'selection',
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox
Header: ({ getToggleAllPageRowsSelectedProps }) => (
<div>
<IndeterminateCheckbox {...getToggleAllPageRowsSelectedProps()} />
</div>
),
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: ({ row }) => (
<div>
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
</div>
),
},
...columns,
])
}
);
const { getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page, //Begins the pagination and select stuff
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
selectedFlatRows,
state: { pageIndex, pageSize, selectedRowIds }} = tableInstance
return (
<div className="container">
{loadingData ? (
<p>Loading Please wait...</p>
) : (
<table {...getTableProps()}>
<thead>
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{
headerGroup.headers.map((column) =>(
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
<span>
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>
</th>
))}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{
page.map((row, i) => {
prepareRow(row);
return(
<tr {...row.getRowProps()}>
{
row.cells.map(cell => {
console.log(cell.render('Cell').props.value);
//<td {...cell.getCellProps()}>{cell.render('Cell').props.value}</td>
return <td>{cell.render('Cell').props.value}</td>
})
}
</tr>
)
})
}
</tbody>
</table>
)}
<div className="pagination">
<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>{' '}
</div>
</div>
)
}
Return <td {...cell.getCellProps()}>{cell.render('Cell')}</td> from tr.
This should work:
<tbody {...getTableBodyProps()}>
{page.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td> // fix return statement
);
})}
</tr>
);
})}
</tbody>
I'm having an issue where react-table (version 7.1.0) seems to be reinitializing any time the page needs to be re-rendered. Using the code below (running example here) as an example, if you were to change the pageIndex value (by switching to a different page), then hit Dummy Button, you can observe that the pageIndex resets back to its default value of 0. The same thing happens if you modify the pageSize in that it automatically resets back to its default value of 10 any time the page has to be re-rendered.
import React, { useState } from "react";
import makeData from "./makeData";
import { useTable, usePagination } from "react-table";
import { ButtonToolbar, Button, Table } from "react-bootstrap";
// Nonsense function to force page to be rendered
function useForceUpdate() {
const [value, setValue] = useState(0);
return () => setValue(value => ++value);
}
export default function App() {
const forceUpdate = useForceUpdate();
const columns = React.useMemo(
() => [
{
Header: "Name",
columns: [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Last Name",
accessor: "lastName"
}
]
},
{
Header: "Info",
columns: [
{
Header: "Age",
accessor: "age"
},
{
Header: "Visits",
accessor: "visits"
},
{
Header: "Status",
accessor: "status"
},
{
Header: "Profile Progress",
accessor: "progress"
}
]
}
],
[]
);
const data = React.useMemo(() => makeData(100000), []);
let ArchiveTable = ({ columns, data }) => {
const {
getTableProps,
getTableBodyProps,
headerGroups,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageOptions,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize }
} = useTable(
{
columns,
data
},
usePagination
);
return (
<div style={{ textAlign: "center" }}>
<Table striped bordered {...getTableProps()} className="datasets">
<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, i) => {
prepareRow(row);
return (
<tr
{...row.getRowProps()}
className={row.isSelected ? "selected" : row.className}
>
{row.cells.map(cell => {
return (
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})}
</tbody>
</Table>
<div className="pagination" style={{ display: "inline-block" }}>
<ButtonToolbar>
<Button
variant="light"
onClick={() => gotoPage(0)}
disabled={!canPreviousPage}
size="small"
>
<span><<</span>
</Button>
<Button
variant="light"
onClick={previousPage}
disabled={!canPreviousPage}
size="small"
>
<span><</span>
</Button>
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
>
{[5, 10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
<Button
variant="light"
onClick={nextPage}
disabled={!canNextPage}
size="small"
>
<span>></span>
</Button>
<Button
variant="light"
onClick={() => gotoPage(pageCount - 1)}
disabled={!canNextPage}
size="small"
>
<span>>></span>
</Button>
</ButtonToolbar>
<span>
Page <strong>{pageOptions.length === 0 ? 0 : pageIndex + 1}</strong>{" "}
of <strong>{pageOptions.length}</strong>
</span>
</div>
</div>
);
};
return (
<div className="App">
<ArchiveTable data={data} columns={columns} />
<button onClick={forceUpdate}>Dummy Button</button>
</div>
);
}
I'm at a complete loss for what to do to fix this. What is the proper way to set everything up so that I don't reinitialize react-table every time the page has to be re-rendered? Said another way, if I hit the Dummy Button, I don't want the table to reset back to page 1 with a page size of 10.
Ended up figuring out the answer. The problem was I needed to move the initialization of the table outside of the rendering logic (quite obvious, in hindsight). Basically, I simply created an ArchiveTable function. For anybody who stumbles across this, you can check here for a working example.
function ArchiveTable({ columns, data }) {
// Add all the initialization and table rendering code here
// (everything that was originally part of the ArchiveTable initialization)
}