'Apply sorting on all table users instead of the users of a single page

I have a table with 2 columns containing users info. I have divided the the table users in multiple pages, so that table of each page only displays 15 users. I have also implemented sorting on this table, so that when I click on each column header, the table is sorted according to this column. Here is the code:

import React, { useState, useEffect } from 'react'
import { getUsers } from '../../services/userService'

const Table = () => {

const [users, setUsers] = useState([]);
const [currentUsers, setCurrentUsers] = useState([]);
const [search, setSearch] = useState('');
const [isSorted, setIsSorted] = useState(false);
const [valueHeader, setValueHeader] = useState({title: "",body: ""}); //Value header state
const [sortedUsers, setSortedUsers] = useState([]);


const pageItemCount = 15
const [pageCount, setPageCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)

useEffect(async () => {
    try {
        const response = await getUsers(search);
        setUsers(response.data.users);
        setPageCount(Math.ceil(response.data.users.length / pageItemCount))
        setCurrentUsers(response.data.users.slice(0, pageItemCount))
    } catch (error) { }
}, [search]);

const sortFn = (userA, userB) => {
  // sort logic here, it can be whatever is needed
  // sorting alphabetically by `first_name` in this case
  return userA[valueHeader.body].localeCompare(userB[valueHeader.body]) //<== Use value of culumn header
}

useEffect(() => {
    if (isSorted) {
      setSortedUsers(currentUsers.slice().sort(sortFn))
    } else {
      setSortedUsers(currentUsers)
    }
  }, [isSorted, currentUsers, valueHeader]) //<== add valueHeader to dependency

const toggleSort = (target) => {
  setIsSorted(!isSorted)
  setValueHeader({
    title: target,
    body: target == "name" ? "first_name" : "mobile_number"
  }) //<=== set state of value header
}

const changePage = (i) => {
    setCurrentPage(i)
    const startItem = ((i - 1) * pageItemCount) + 1
    setCurrentUsers(users.slice(startItem - 1, (pageItemCount * i)))
}

const handleChange = (event, value) => {
    changePage(value);
}

    return (
        <div dir='rtl' className='bg-background mt-10 px-5 rd1200:px-30 overflow-auto'>
           
            <table className='w-full border-separate rounded-md'>
                <thead>
                    <tr className='bg-text-secondary text-white shadow-sm text-center'>
                        <th className='p-2' onClick={()=>toggleSort("name")}>name</th>
                        <th className='p-2' onClick={()=>toggleSort("mobile")}>mobile</th>
                    </tr>
                </thead>
                <tbody>
                    {sortedUsers.map((item, index) =>
                        <tr key={item.id} className={index % 2 === 0 ? 'bg-white shadow-sm text-center' : 'bg-text bg-opacity-5 shadow-sm text-center'}>
                            <td className='text-text text-sm p-2'>{item.first_name}</td>
                            <td className='text-text text-sm p-2'>{item.mobile_number}</td> 
                        </tr>
                    )}
                </tbody>
            </table>
            <Pagination className="mt-2 pb-20" dir='ltr' page={currentPage} count={pageCount} onChange={handleChange} variant="outlined" shape="rounded" />                
        </div>
    )
}

export default Table

The only problem is that, since I display only 15 users of the table in each page, when I click on the column header, only the users of that page is sorted, but I want to apply sorting on all users of the table (the users of all pages). Is it possible?

Edited code according to suggested answer:

    import React, { useState, useEffect } from 'react'
    import { getUsers } from '../../services/userService'
    
    const Table = () => {
    
    const [users, setUsers] = useState([]);
    const [sortDirection, setSortDirection] = useState("asc");
    const [search, setSearch] = useState('');
    
    
    const pageItemCount = 15
    const [pageCount, setPageCount] = useState(2);
    const [currentPage, setCurrentPage] = useState(1);
    
    useEffect(async () => {
        try {
            const response = await getUsers(search);
            setUsers(response.data.users);
            setPageCount(Math.ceil(response.data.users.length / pageItemCount));
        } catch (error) { }
    }, [search]);

    useEffect(() => {
         toggleSort("name");
    }, [])

    const startItem = (currentPage - 1) * pageItemCount + 1;
    const pagedUsers = users.slice(startItem - 1, pageItemCount * currentPage);

  const sortFn = (fieldToSort, direction) => (userA, userB) => {
    if (direction === "asc")
      return userA[fieldToSort].localeCompare(userB[fieldToSort]);
    else return userB[fieldToSort].localeCompare(userA[fieldToSort]);
  };
    
    const toggleSort = (target) => {
    const direction = sortDirection === "asc" ? "desc" : "asc";  
    const fieldToSort = target === "name" ? "first_name" : "mobile_number";   
    setSortDirection(direction);    
    setUsers(users.slice().sort(sortFn(fieldToSort, direction)));
  };
    
  const handleChange = (event, value) => {
    setCurrentPage(value);
  };
    
        return (
            <div dir='rtl' className='bg-background mt-10 px-5 rd1200:px-30 overflow-auto'>
               
                <table className='w-full border-separate rounded-md'>
                    <thead>
                        <tr className='bg-text-secondary text-white shadow-sm text-center'>
                            <th className='p-2' onClick={()=>toggleSort("name")}>name</th>
                            <th className='p-2' onClick={()=>toggleSort("mobile")}>mobile</th>
                        </tr>
                    </thead>
                    <tbody>
                        {pagedUsers?.map((item, index) =>
                            <tr key={item.id} className={index % 2 === 0 ? 'bg-white shadow-sm text-center' : 'bg-text bg-opacity-5 shadow-sm text-center'}>
                                <td className='text-text text-sm p-2'>{item.first_name}</td>
                                <td className='text-text text-sm p-2'>{item.mobile_number}</td> 
                            </tr>
                        )}
                    </tbody>
                </table>
                {users.length > 0 && (
        <Pagination
          className="mt-2 pb-20"
          dir="ltr"
          page={currentPage}
          count={pageCount}
          onChange={handleChange}
          variant="outlined"
          shape="rounded"
        />
      )}
            </div>
        )
    }
    
    export default Table


Solution 1:[1]

You have 2 methods the first is sorting the users array but this will change the users in this page The second is wich i prefere is to call the sorting function in the changepage function

Solution 2:[2]

The error it's in this useEffect logic:

useEffect(() => {
    if (isSorted) {
      // When its sorted, you are setting to sort currentUsers,
      // and it holds only the first 15 users, not the entire users array
      setSortedUsers(currentUsers.slice().sort(sortFn))
    } else {
      setSortedUsers(currentUsers)
    }
  }, [isSorted, currentUsers, valueHeader]) 

The below code should work for you:

useEffect(() => {
    if (isSorted) {

      // get all users and sort it, 
      // returning an immutable array because the use of .slice()
      const usersUpdated = users.slice().sort(sortFn).slice(0, pageItemCount);

      // Updated the currentUsers and sortedUsers states
      setSortedUsers(usersUpdated);
      setCurrentUsers(usersUpdated);
    } else {
      // Updated the currentUsers and sortedUsers states with the first 15 users
      setSortedUsers(users.slice(0, pageItemCount));
      setCurrentUsers(users.slice(0, pageItemCount));
    }
  // instead call the useEffect base on currentUsers, you change it to users
  }, [isSorted, users, valueHeader]);

Having said that, just a point - You are using to many states for user:
. one for all users
. one for current users
. one for sorted users

You can handle this with only one state, i did a code sample to you check it.

Solution 3:[3]

The main cause of your issue is the line:

setSortedUsers(currentUsers.slice().sort(sortFn))

when I click on the column header, only the users of that page is sorted

It's not that strange that only the users on the current page are sorted, since that's exactly what the line of code above does.

Instead you want to sort first, then take the currentUsers from the sortedUsers.

I've written an answer, but did overhaul your code. The reason being is that you violate the single source of truth principal. Often resulting in bugs or strange behaviour due to a mismatch between the different sources of truth.


Examples of source of truth duplication is the fact that you store 3 lists of users users, currentUsers, and sortedUsers. The pageCount is essentially stored 2 times. One can be calculated by Math.ceil(users.length / pageItemCount), the other is stored in the pageCount state.

What happens if you change the users array length, but forget to adjust the pageCount?

Instead of storing the pageCount in a state, you can derive it from two available values. So there is no need for a state, instead use useMemo.

const pageCount = useMemo(() => (
  Math.ceil(users.length, pageItemCount)
), [users.length, pageItemCount]);

Similarly you could derive sortedUsers and currentUsers from users. If you have access to the order in which the users should be sorted, the current page, and the maximum page size.

I've extracted some of the logic into separate functions to keep the component itself somewhat clean. Since you haven't given us a snippet or environment to work with I'm not sure if the code below works. But it should hopefully give you some inspiration/insight on how to handle things.

import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { getUsers } from '../../services/userService';

// Returns a new object without the given keys.
function without(object, ...excludeKeys) {
  excludeKeys = new Set(excludeKeys);
  
  return Object.fromEntries(
    Object.entries(object).filter(([key]) => !excludeKeys.has(key))
  );
}

// Compares the given property value in both users.
// Returns -1, 0, or 1, based on ascending comparison.
function compareUserProp(userA, userB, prop) {
  const valueA = userA[prop];
  const valueB = userB[prop];

  if (typeof valueA === "string" && typeof valueB === "string") {
    return valueA.localeCompare(valueB);
  }
  
  if (valueA < valueB) return -1;
  if (valueA > valueB) return  1;
  return 0;
}

function isEven(integer) {
  return integer % 2 === 0;
}

function getUserTRClass(index) {
  if (isEven(index)) {
    return 'bg-white shadow-sm text-center';
  } else {
    return 'bg-text bg-opacity-5 shadow-sm text-center';
  }
}

function Table({ maxPageSize = 15 }) {
  const [users      , setUsers      ] = useEffect([]); // [{ first_name: "John", last_name: "Doe", age: 42 }]
  const [search     , setSearch     ] = useEffect("");
  const [order      , setOrder      ] = useEffect({}); // { last_name: "asc", age: "desc" }
  const [currentPage, setCurrentPage] = useEffect(1);

  const pageCount = useMemo(() => (
    Math.ceil(users.length / maxPageSize)
  ), [users.length, maxPageSize]);

  const sortedUsers = useMemo(() => {
    const modifier = { asc: 1, desc: -1 };

    return Array.from(users).sort((userA, userB) => {
      for (const [prop, direction] of Object.entries(order)) {
        const diff = compareUserProp(userA, userB, prop);
        if (diff) return diff * modifier[direction];
      }
      return 0;
    });
  }, [users, order]);

  const usersOnPage = useMemo(() => {
    const zeroBasedPage = currentPage - 1;
    const beginIndex    = zeroBasedPage * maxPageSize;
    const endIndex      = beginIndex + maxPageSize;
    
    return sortedUsers.slice(beginIndex, endIndex);
  }, [sortedUsers, currentPage, maxPageSize]);

  // Do not pass an async function directly to `useEffect`. `useEffect` expects
  // a cleanup function or `undefined` as the return value. Not a promise.
  useEffect(() => {
    (async function () {
      const response = getUsers(search);
      setUsers(response.data.users);
      // setCurrentPage(1); // optional, reset page to 1 after a search
    })();
  }, [search]);

  const toggleSort = useCallback((prop) => {
    const inverse = { "desc": "asc", "asc": "desc" };

    setOrder((order) => {
      const direction = order[prop] || "desc";
      return { [prop]: inverse[direction], ...without(order, prop) };
    });
  }, []);

  const changePage = useCallback((_event, newPage) => {
    setCurrentPage(newPage);
  }, []);

  return (
    <div dir='rtl' className='bg-background mt-10 px-5 rd1200:px-30 overflow-auto'>

      <table className='w-full border-separate rounded-md'>
        <thead>
          <tr className='bg-text-secondary text-white shadow-sm text-center'>
            <th className='p-2' onClick={() => toggleSort("first_name")}>name</th>
            <th className='p-2' onClick={() => toggleSort("mobile_number")}>mobile</th>
          </tr>
        </thead>
        <tbody>
          {usersOnPage.map((user, index) => (
            <tr key={user.id} className={getUserTRClass(index)}>
              <td className='text-text text-sm p-2'>{user.first_name}</td>
              <td className='text-text text-sm p-2'>{user.mobile_number}</td> 
            </tr>
          ))}
        </tbody>
      </table>
      <Pagination className="mt-2 pb-20" dir='ltr' page={currentPage} count={pageCount} onChange={changePage} variant="outlined" shape="rounded" />
    </div>
  );
}

export default Table;

If you want to start the users of sorted simply set an initial value for order. For example:

const [order, setOrder] = useState({ last_name: "asc" });

Solution 4:[4]

The algorithm to solve this would be as follows

  • Maintain one state for your entire dataset.

    state: allUsers

  • Capture the event of button click.

  • Applying sorting to the entire data based on event handler inputs you can decide the sort criterion.

    allUsers.sort(criterionFunction); // you may call an API for this step and bind result to allUsers if needed or do it on the client side.

  • Derive the slice of data set based on the limit and offset maintained in the local state.

    usersInPage = allUsers.slice(offset,limit)

  • The derived data slice shall re-render itself on the pagination UI.

    renderUsers(usersInPage)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Mohamad Al Zohbie
Solution 2 Luis Paulo Pinto
Solution 3
Solution 4 Mayank Narula