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