1

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

  • When I click next page I'm updating the "currentPage" state with "onChangePage" event of data table.
  • Because "onChangeRowsPerPage" event triggers first before "onChangePage" event and reset selected items before page change, I always end up losing the previous page selected items.

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>
);
}
senerdude
  • 98
  • 1
  • 7

1 Answers1

3

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>
  );
}
Paulo Fernando
  • 3,148
  • 3
  • 5
  • 21
  • thanks, genius workaround! Essentially I'll keep these in state manager like redux or reactive variable. when the user leaves the page and comes back I have to pre-select these rows. At the moment if I set "selectedRowsPerPage" it throws an error. "selectedPerPage.forEach is not a function" – senerdude Dec 16 '22 at 10:12
  • Glad to help! Hmm don't know what might be going on, can you make a sandbox with what you are trying to do? – Paulo Fernando Dec 16 '22 at 14:52
  • For example : useEffect(() => { const preSelectedItems = [{"id":4,"email":"eve.holt@reqres.in","first_name":"Eve","last_name":"Holt","avatar":"reqres.in/img/faces/4-image.jpg"},{"id":3,"email":"emma.wong@reqres.in","first_name":"Emma","last_name":"Wong","avatar":"reqres.in/img/faces/3-image.jpg"}] setSelectedRowsPerPage(preSelectedItems) }, []) and if I add this : selectedPerPage.forEach is not a function – senerdude Dec 16 '22 at 15:32
  • 1
    It's because selectedRowsPerPage is controlled by page, I edited the answer and the sandbox, and left some comments, let me know if you understood ;) This may be a problem because to define the preselected rows you need to know the page where the row is... If for some reason the data changes and the page number where the row was changes after you stored, when the user comes back to the page the selected wont match anymore. If it's the case, then unfortunately you will have to use another data table component. – Paulo Fernando Dec 16 '22 at 17:01
  • 1
    Also, another thing that may be a problem in mobile, mouse enter and leave doesn't work very well there... – Paulo Fernando Dec 16 '22 at 18:15
  • 1
    Hmm I think the way for it to work properly is for example, when the user selects a row, you send an ajax request to persist that in the database, so when the pages are fetched, they already come with the seleted status from the database, that way, workarounds wont me necessary – Paulo Fernando Dec 16 '22 at 18:21
  • The database is not an option for me but I think this workaround solves the problem except for mobile but that's not a priority anyway. Thank you so much! – senerdude Dec 19 '22 at 09:55