React Table 7 - Expand Rows on Table Load - reactjs

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;
}
}, []);

Related

How can I Mark Initial value of Checkbox Checked of a single row in React tabl

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

Global Filter in react-table v8 isn't working

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.

Why arent these functions within the main functional component?

I don't understand why these formatter functions are placed outside the main component called TemplateTable.
If you need to use the actionformatter for example, you cannot pass a dispatch function do it correct?
I had an issue trying to set the actionformatter onClick for the delete button to a dispatch(deleteTemplate()).
When I run the code while the actionformatter is outside the main component TemplateTable, I get dispatch is undefined. When I defined dispatch within the component I obviously get the cannot use react hooks ouside function component problem.
I can fix this whole issue by just including the actionformatter inside the block of templateTable. I just feel like im shortcutting and was wondering if anyone had any input on this
import React, { createRef, Fragment, useState, useEffect} from 'react';
import {
Button,
Card,
CardBody,
Col,
CustomInput,
DropdownItem,
DropdownMenu,
DropdownToggle,
InputGroup,
Media,
Modal,
ModalBody,
Row,
UncontrolledDropdown
} from 'reactstrap';
import { connect, useDispatch } from 'react-redux';
import FalconCardHeader from '../common/FalconCardHeader';
import ButtonIcon from '../common/ButtonIcon';
import paginationFactory, { PaginationProvider } from 'react-bootstrap-table2-paginator';
import BootstrapTable from 'react-bootstrap-table-next';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { Link } from 'react-router-dom';
import Flex from '../common/Flex';
import Avatar from '../common/Avatar';
import { getPaginationArray } from '../../helpers/utils';
import CreateTemplate from '../templates/CreateTemplate';
import customers from '../../data/e-commerce/customers';
import { listTemplates, deleteTemplate } from '../../actions/index';
const nameFormatter = (dataField, { template }) => {
return (
<Link to="/pages/customer-details">
<Media tag={Flex} align="center">
<Media body className="ml-2">
<h5 className="mb-0 fs--1">{template}</h5>
</Media>
</Media>
</Link>
);
};
const bodyFormatter = (dataField, { avatar, body }) => {
return (
<Link to="/pages/customer-details">
<Media tag={Flex} align="center">
<Media body className="ml-2">
<h5 className="mb-0 fs--1">{body}</h5>
</Media>
</Media>
</Link>
);
};
const emailFormatter = email => <a href={`mailto:${email}`}>{email}</a>;
const phoneFormatter = phone => <a href={`tel:${phone}`}>{phone}</a>;
const actionFormatter = (dataField, { _id }) => (
// Control your row with this id
<UncontrolledDropdown>
<DropdownToggle color="link" size="sm" className="text-600 btn-reveal mr-3">
<FontAwesomeIcon icon="ellipsis-h" className="fs--1" />
</DropdownToggle>
<DropdownMenu right className="border py-2">
<DropdownItem onClick={() => console.log('Edit: ', _id)}>Edit</DropdownItem>
<DropdownItem onClick={} className="text-danger">
Delete
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
);
const columns = [
{
dataField: 'name',
text: 'Name',
headerClasses: 'border-0',
classes: 'border-0 py-2 align-middle',
formatter: nameFormatter,
sort: true
},
{
dataField: 'content',
headerClasses: 'border-0',
text: 'Content',
classes: 'border-0 py-2 align-middle',
formatter: bodyFormatter,
sort: true
},
{
dataField: 'joined',
headerClasses: 'border-0',
text: 'Last modified',
classes: 'border-0 py-2 align-middle',
sort: true,
align: 'right',
headerAlign: 'right'
},
{
dataField: '',
headerClasses: 'border-0',
text: 'Actions',
classes: 'border-0 py-2 align-middle',
formatter: actionFormatter,
align: 'right'
}
];
const SelectRowInput = ({ indeterminate, rowIndex, ...rest }) => (
<div className="custom-control custom-checkbox">
<input
className="custom-control-input"
{...rest}
onChange={() => {}}
ref={input => {
if (input) input.indeterminate = indeterminate;
}}
/>
<label className="custom-control-label" />
</div>
);
const selectRow = onSelect => ({
mode: 'checkbox',
columnClasses: 'py-2 align-middle',
clickToSelect: false,
selectionHeaderRenderer: ({ mode, ...rest }) => <SelectRowInput type="checkbox" {...rest} />,
selectionRenderer: ({ mode, ...rest }) => <SelectRowInput type={mode} {...rest} />,
headerColumnStyle: { border: 0, verticalAlign: 'middle' },
selectColumnStyle: { border: 0, verticalAlign: 'middle' },
onSelect: onSelect,
onSelectAll: onSelect
});
const TemplateTable = ( props ) => {
let table = createRef();
// State
const [isSelected, setIsSelected] = useState(false);
const [showTemplateModal, setShowTemplateModal] = useState(false);
const handleNextPage = ({ page, onPageChange }) => () => {
onPageChange(page + 1);
};
const dispatch = useDispatch()
const deleteHandler = (_id) => {
dispatch(deleteHandler())
}
useEffect(() => {
dispatch(listTemplates())
}, [])
const handlePrevPage = ({ page, onPageChange }) => () => {
onPageChange(page - 1);
};
const onSelect = () => {
setImmediate(() => {
setIsSelected(!!table.current.selectionContext.selected.length);
});
};
const options = {
custom: true,
sizePerPage: 12,
totalSize: props.templates.length
};
return (
<Card className="mb-3">
<FalconCardHeader light={false}>
{isSelected ? (
<InputGroup size="sm" className="input-group input-group-sm">
<CustomInput type="select" id="bulk-select">
<option>Bulk actions</option>
<option value="Delete">Delete</option>
<option value="Archive">Archive</option>
</CustomInput>
<Button color="falcon-default" size="sm" className="ml-2">
Apply
</Button>
</InputGroup>
) : (
<Fragment>
<ButtonIcon onClick={(() => setShowTemplateModal(true))}icon="plus" transform="shrink-3 down-2" color="falcon-default" size="sm">
New Template
</ButtonIcon>
<Modal isOpen={showTemplateModal} centered toggle={() => setShowTemplateModal(!showTemplateModal)}>
<ModalBody className="p-0">
<Card>
<CardBody className="fs--1 font-weight-normal p-4">
<CreateTemplate />
</CardBody>
</Card>
</ModalBody>
</Modal>
<ButtonIcon icon="fa-download" transform="shrink-3 down-2" color="falcon-default" size="sm" className="mx-2">
Download
</ButtonIcon>
<ButtonIcon icon="external-link-alt" transform="shrink-3 down-2" color="falcon-default" size="sm">
Expand View
</ButtonIcon>
</Fragment>
)}
</FalconCardHeader>
<CardBody className="p-0">
<PaginationProvider pagination={paginationFactory(options)}>
{({ paginationProps, paginationTableProps }) => {
const lastIndex = paginationProps.page * paginationProps.sizePerPage;
return (
<Fragment>
<div className="table-responsive">
<BootstrapTable
ref={table}
bootstrap4
keyField="_id"
data={props.templates}
columns={columns}
selectRow={selectRow(onSelect)}
bordered={false}
classes="table-dashboard table-striped table-sm fs--1 border-bottom border-200 mb-0 table-dashboard-th-nowrap"
rowClasses="btn-reveal-trigger border-top border-200"
headerClasses="bg-200 text-900 border-y border-200"
{...paginationTableProps}
/>
</div>
<Row noGutters className="px-1 py-3 flex-center">
<Col xs="auto">
<Button
color="falcon-default"
size="sm"
onClick={handlePrevPage(paginationProps)}
disabled={paginationProps.page === 1}
>
<FontAwesomeIcon icon="chevron-left" />
</Button>
{getPaginationArray(paginationProps.totalSize, paginationProps.sizePerPage).map(pageNo => (
<Button
color={paginationProps.page === pageNo ? 'falcon-primary' : 'falcon-default'}
size="sm"
className="ml-2"
onClick={() => paginationProps.onPageChange(pageNo)}
key={pageNo}
>
{pageNo}
</Button>
))}
<Button
color="falcon-default"
size="sm"
className="ml-2"
onClick={handleNextPage(paginationProps)}
disabled={lastIndex >= paginationProps.totalSize}
>
<FontAwesomeIcon icon="chevron-right" />
</Button>
</Col>
</Row>
</Fragment>
);}
}
</PaginationProvider>
</CardBody>
</Card>
);
};
const mapStateToProps = (state) => {
return {
templates: state.templates,
auth: state.auth,
deleteTemplate: state.deleteTemplate
}
}
export default connect(mapStateToProps, { listTemplates })(TemplateTable);
The short answer is that as-is there isn't any dependency on anything from a consuming component, so it makes complete sense to externalize these declarations from the TemplateTable component.
While you could just move code back into the component to close over any dependencies in the enclosure of the functional component body I think we can do better.
I suggest currying the dispatch function to any of the specific formatters that need it, i.e.
const actionFormatter = ({ dispatch }) => (dataField, { _id }) => (
// Control your row with this id
<UncontrolledDropdown>
<DropdownToggle color="link" size="sm" className="text-600 btn-reveal mr-3">
<FontAwesomeIcon icon="ellipsis-h" className="fs--1" />
</DropdownToggle>
<DropdownMenu right className="border py-2">
<DropdownItem onClick={() => console.log('Edit: ', _id)}>Edit</DropdownItem>
<DropdownItem
onClick={() => dispatch(deleteTemplate())}
className="text-danger"
>
Delete
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
);
And turn columns into a factory function in order to receive and pass on configurations, i.e.
const columns = ({ dispatch }) => ([ // <-- consume config & destructure
{
dataField: 'name',
text: 'Name',
headerClasses: 'border-0',
classes: 'border-0 py-2 align-middle',
formatter: nameFormatter,
sort: true
},
{
dataField: 'content',
headerClasses: 'border-0',
text: 'Content',
classes: 'border-0 py-2 align-middle',
formatter: bodyFormatter,
sort: true
},
{
dataField: 'joined',
headerClasses: 'border-0',
text: 'Last modified',
classes: 'border-0 py-2 align-middle',
sort: true,
align: 'right',
headerAlign: 'right'
},
{
dataField: '',
headerClasses: 'border-0',
text: 'Actions',
classes: 'border-0 py-2 align-middle',
formatter: actionFormatter({ dispatch }), // <-- pass dispatch
align: 'right'
}
]);
Create the columns configuration object and pass to the table.
const config = { dispatch };
...
<BootstrapTable
ref={table}
bootstrap4
keyField="_id"
data={props.templates}
columns={columns(config)} // <-- pass config to factory
selectRow={selectRow(onSelect)}
bordered={false}
classes="table-dashboard table-striped table-sm fs--1 border-bottom border-200 mb-0 table-dashboard-th-nowrap"
rowClasses="btn-reveal-trigger border-top border-200"
headerClasses="bg-200 text-900 border-y border-200"
{...paginationTableProps}
/>

React-table combination of rows and pagination doesn't function as intended

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.

react-table Reinitializing on Render

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)
}

Resources