reactjsreact-data-table-component

react-data-table-component onChangePage method triggers after onSelectedRowsChange method


I'm trying to keep track of selected items by page.

Here is the codesandbox link: https://codesandbox.io/s/affectionate-forest-tx3309?file=/src/App.js

No matter what I tried, when I click the next page it triggers "onChangeRowsPerPage" first and removes all selected items. Any ideas on how can I solve this problem?

Thanks!

import { useEffect, useMemo, useState } from "react";
import axios from "axios";
import DataTable from "react-data-table-component";
import "./styles.css";

export default function App() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [totalRows, setTotalRows] = useState(0);
const [perPage, setPerPage] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
const [selectedRows, setSelectedRows] = useState([]);

const fetchUsers = async (page) => {
  setLoading(true);
  const response = await axios.get(`https://reqres.in/api/users?page=${page}&per_page=${perPage}&delay=1`);
  setData(response.data.data);
  setTotalRows(response.data.total);
  setLoading(false);
};

const handlePageChange = (page) => {
  setCurrentPage(page);
  fetchUsers(page);
};

const handlePerRowsChange = async (newPerPage, page) => {
  setLoading(true);

  const response = await axios.get(`https://reqres.in/api/users?page=${page}&per_page=${newPerPage}&delay=1`);

  setData(response.data.data);
  setPerPage(newPerPage);
  setLoading(false);
};

const handleRowSelected = async (row) => {
  console.log(row, currentPage);

  // Check current page has selected items
  const selectedPageRowIndex = selectedRows.findIndex((row) => row.page === currentPage);

  // If there is no selected records for this page
  if (selectedPageRowIndex === -1) {
    const tmpRow = { ...row }; // Copy returned row object
    tmpRow.page = currentPage; // Set page
    setSelectedRows([...selectedRows, tmpRow]); // Update state
  } else {
    // If exist, update

    console.log("Current page :", currentPage, " updating...");

    const tmpSelectedPageRows = [...selectedRows]; // Copy state

    tmpSelectedPageRows[selectedPageRowIndex].selectedRows = row.selectedRows; // Update selected rows

    setSelectedRows(tmpSelectedPageRows); // Update state
  }
};

useEffect(() => {
  fetchUsers(1); // fetch page 1 of users
}, []); // eslint-disable-line react-hooks/exhaustive-deps

// Table Column Configuration
const columns = useMemo(() => [
  {
    name: "Avatar",
    cell: (row) => (
      <img height="30px" width="30px" alt={row.first_name} src={row.avatar} />
  )
  },
  {
    name: "First Name",
    selector: (row) => row.first_name
  },
  {
    name: "Last Name",
    cell: (row) => row.last_name
  },
  {
    name: "Email",
    selector: (row) => row.email
  }
]);

return (
  <div className="App">
    <h1>react-data-table-component</h1>
    <p>with remote pagination + pre/selected rows</p>

    {JSON.stringify(selectedRows, null, 2)}

    <DataTable
      title="Users"
      columns={columns}
      data={data}
      progressPending={loading}
      pagination
      paginationServer
      paginationTotalRows={totalRows}
      onChangeRowsPerPage={handlePerRowsChange}
      onChangePage={handlePageChange}
      selectableRows
      onSelectedRowsChange={handleRowSelected}
    />
  </div>
);
}

Solution

  • I had to go into heavy workaround mode to do this, the component doesn't nicely support server side pagination with selectable rows.

    Take a look:

    https://codesandbox.io/s/flamboyant-tesla-22fbq7?file=/src/App.js

    import { useCallback, useEffect, useMemo, useState } from "react";
    import axios from "axios";
    import DataTable from "react-data-table-component";
    
    export default function App() {
      const [data, setData] = useState([]);
      const [loading, setLoading] = useState(false);
      const [totalRows, setTotalRows] = useState(10);
      const [action] = useState({ fromUser: false }); //this is a way to have an instant-changing state
      const [rowsPerPage, setRowsPerPage] = useState(4); //change to 10 after you remove paginationRowsPerPageOptions
      const [currentPage, setCurrentPage] = useState(1);
      const [selectedRowsPerPage, setSelectedRowsPerPage] = useState([]);
    
      const fetchUsers = async (page, rowsPerPage) => {
        setLoading(true);
        const response = await axios.get(
          `https://reqres.in/api/users?page=${page}&per_page=${rowsPerPage}&delay=1`
        );
        setData(response.data.data);
        setTotalRows(response.data.total);
        setLoading(false);
      };
    
      const handlePageChange = (page) => {
        fetchUsers(page, rowsPerPage);
        setCurrentPage(page);
      };
    
      const handleRowsPerPageChange = async (newRowsPerPage) => {
        if (!data.length) return; //when the table is rendered for the first time, this would trigger, and we don't need to call fetchUsers again
        fetchUsers(1, newRowsPerPage);
        setRowsPerPage(newRowsPerPage);
        setCurrentPage(1);
        setSelectedRowsPerPage([]);
      };
    
      const handleOnSelectedRowsChange = useCallback(
        ({ selectedRows }) => {
          if (!action.fromUser) return; //the component always trigger this with 0 selected rows when it renders a page, what would clear the selection
    
          selectedRowsPerPage[currentPage] = selectedRows; //there is no way to tell if a row was DEselected, so I had to control the selected rows per page,
          //the array would get an index to control each page
          console.log(JSON.stringify(selectedRowsPerPage));
        },
        [currentPage, selectedRowsPerPage, action.fromUser]
      );
    
      const handleMouseEnter = () => {
        action.fromUser = true; //this was the way I found to prevent the component to clear the selection on every page render,
        //if the user is not with the mouse on a row, doesn't allow to change the selected rows
      };
    
      const handleMouseLeave = () => {
        action.fromUser = false; //When the users moves the mouse out of a row, block the changes to the selected rows array (line 39)
      };
    
      const getAllSelectedRows = () => {
        //if you need to get all of the selected rows in all pages, call this
        const allSelected = [];
        selectedRowsPerPage.forEach((selectedPerPage) => {
          if (selectedPerPage) {
            selectedPerPage.forEach((selectRow) => {
              allSelected.push(selectRow);
            });
          }
        });
        return allSelected;
      };
    
      //this applies the selected rows on the page renders, it checks if the id of the row exists in the array
      const handleApplySelectedRows = (row) =>
        selectedRowsPerPage[currentPage]?.filter(
          (selectedRow) => selectedRow.id === row.id
        ).length > 0;
    
      useEffect(() => {
        //it's controlled by page, for example selectedRowsPerPage[1] contains the selected rows for page 1 and so forth...
        const preSelectedItems = [
          //index 0 is always null, because pages start at 1
          null,
    
          //index 1 in the selected for fir the first page:
          [
            {
              id: 3,
              email: "emma.wong@reqres.in",
              first_name: "Emma",
              last_name: "Wong",
              avatar: "https://reqres.in/img/faces/3-image.jpg"
            },
            {
              id: 1,
              email: "george.bluth@reqres.in",
              first_name: "George",
              last_name: "Bluth",
              avatar: "https://reqres.in/img/faces/1-image.jpg"
            }
          ],
    
          //index 2 null for example, because nothing is selected for page 2
          null,
    
          //index 3, one selected for page 3
          [
            {
              id: 11,
              email: "george.edwards@reqres.in",
              first_name: "George",
              last_name: "Edwards",
              avatar: "https://reqres.in/img/faces/11-image.jpg"
            }
          ]
        ];
    
        setSelectedRowsPerPage(preSelectedItems);
      }, []);
    
      const columns = useMemo(
        () => [
          {
            name: "Avatar",
            cell: (row) => (
              <img
                height="30px"
                width="30px"
                alt={row.first_name}
                src={row.avatar}
              />
            )
          },
          {
            name: "First Name",
            selector: (row) => row.first_name
          },
          {
            name: "Last Name",
            cell: (row) => row.last_name
          },
          {
            name: "Email",
            selector: (row) => row.email
          }
        ],
        []
      );
    
      return (
        <div className="App">
          <h1>react-data-table-component</h1>
    
          <h2>Users</h2>
          {
            //had to remove the title in the DataTable, because the select count was only regarging the current page not all selected rows
          }
    
          <DataTable
            pagination
            paginationServer
            selectableRows
            columns={columns}
            data={data}
            progressPending={loading}
            paginationTotalRows={totalRows}
            selectableRowsNoSelectAll={true} //I needed to remove the select all, because it would not work due to the mouse enter/leave workaround
            paginationDefaultPage={currentPage}
            paginationRowsPerPageOptions={[4, 8, 15]} //you can remove it later, just to have more pages
            paginationPerPage={rowsPerPage}
            onRowMouseEnter={handleMouseEnter}
            onRowMouseLeave={handleMouseLeave}
            onChangePage={handlePageChange}
            onChangeRowsPerPage={handleRowsPerPageChange}
            onSelectedRowsChange={handleOnSelectedRowsChange}
            selectableRowSelected={handleApplySelectedRows}
          />
    
          <button
            onClick={() => {
              setSelectedRowsPerPage([...selectedRowsPerPage]);
            }}
          >
            Refresh All Selected
          </button>
          <br />
          <br />
          {JSON.stringify(getAllSelectedRows())}
        </div>
      );
    }