reactjsreact-table-v7

React Table 7 - Expand Rows on Table Load


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"
          }
        ]
      }
    ]
  }
};


Solution

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