Sorting and filtering in React applications

In order to do sorting and filtering declaratively (which is desirable), you should avoid storing results of sorting in state. Instead, put those things in state which can lead you to this result. This approach should be taken for all methods that are variations on existing data, given these methods are synchronous. Failing to follow this rule and putting the results of sorting in state breaks the rule of a single source of truth, and therefore the premise that all that is rendered, is a reflection of state.

rendered = fn(state);

Exceptions

There is an exceptions to this rule. Asynchronous methods may use state as a caching mechanism for the results, as re-calculating them would result in considerable costs, such as extra time, network calls, etc. Optimally, the window where such asynchronous results are not in sync with the dependent state should be taken into account. For example, you may inform the user if the results of a sort are out of sync with the dependent state by showing a loading indicator.

Sorting

Let's say you are sorting a table; and the column that is currently used for sorting, has a little arrow that is pointing up or down.

rowData; // Contains all rows in the table along with their data per column
sortingColumn; // The colum to sort
sortingDirection; // The direction to sort this column (ascending or descending)

There are 3 variables here: rowData, sortingColumn and sortingDirection. If you are storing these variables in state, together with the sorted array, you now have 2 sources of truth: the states sortingColumn and sortingDirection are also encoded in the indexes of the rowData array. Now you have 2 choices (actually only 1, but let's explore directions too): Either remove sortingColumn and sortingDirection or stop encoding sorting into rowData state.

Should we drop sortingColumn and sortingDirection?

If you drop the states for sortingColumn and sortingDirection, your application will not be declarative anymore. Indeed, the column selection and arrow would live somewhere else, outside of the React state. What is rendered will not be a function of state anymore. This will make an application unpredictable.

Stop encoding sorting into rowData

Encoding sorting into rowData is problematic, because you may in the future introduce new code that also changes the rowData state. There is no guarantee that this new code will take into account this existing sorting mechanism that are already in place. Why would they? There is no indication that this array is sorted, apart from the existing state. This new code would result in invalid states. A proper state model means making invalid states impossible.

The prefered approach would then be to not store the sorting results in state, but to calculate them on the fly. Then you will not have to worry about keeping the rowData sorting in sync with the other state variables.

function App() {
  const [sortingColumn, setSortingColumn] = useState(null);
  const [sortingDirection, setSortingDirection] = useState('asc');
  const [rowData, setRowData] = useState([]);

  const sort = (column) => {
    if (sortingColumn === column) {
      setSortingDirection(sortingDirection === 'asc' ? 'desc' : 'asc');
    } else {
      setSortingColumn(column);
      setSortingDirection('asc');
    }
  };

  // The sortedRowData is calculated on every render.
  const sortedRowData = sortData(rowData, sortingColumn, sortingDirection);

Memoizing rowData

The approach above will call sortData on every render, which will have some performance overhead.

Instead, you can use a memoization. Memoizing rowData will make sure that sorting is only done when the sorting column or direction changes (as well as on mount).

function App() {
  const [sortingColumn, setSortingColumn] = useState(null);
  const [sortingDirection, setSortingDirection] = useState('desc');
  const [rowData, setRowData] = useState([]);

  const sort = (column) => {
    if (sortingColumn === column) {
      setSortingDirection(sortingDirection === 'asc' ? 'desc' : 'asc');
    } else {
      setSortingColumn(column);
      setSortingDirection('asc');
    }
  };

  const sortedRowData = useMemo(() => sortData(rowData, sortingColumn, sortingDirection), [rowData, sortingColumn, sortingDirection]);

By the way, the implementation of the sortData function is as follows

function sortData(rowData, sortingColumn, sortingDirection) {
  if (sortingColumn === null) {
    return rowData;
  }

  return rowData.slice().sort((a, b) => {
    const aValue = a[sortingColumn];
    const bValue = b[sortingColumn];

    if (aValue === bValue) {
      return 0;
    }

    if (sortingDirection === "asc") {
      return aValue < bValue ? -1 : 1;
    } else {
      return aValue < bValue ? 1 : -1;
    }
  });
}

Filtering rowData

The prolem becomes even more apparent when you want to filter the data. Filtering is a more destructive operation than sorting, because it will remove rows from the data. A sub-optimal approach would again be to store the filtered data in state, and sync filtered state with other state variables such as sorting variables and the rowData itself.

The strategy you should take instead is equal to the one above, but instead of only sorting, you will also filter.

Let's say we want to filter the data based on user input. The logic could then look as follows:

function App() {
  const [sortingColumn, setSortingColumn] = useState(null);
  const [sortingDirection, setSortingDirection] = useState('asc');
  const [userInput, setUserInput] = useState('');
  const [rowData, setRowData] = useState([]);

  const sort = (column) => {
    if (sortingColumn === column) {
      setSortingDirection(sortingDirection === 'asc' ? 'desc' : 'asc');
    } else {
      setSortingColumn(column);
      setSortingDirection('asc');
    }
  };

  const sortedAndFilteredData = useMemo(() => {
    const filteredData = filterData(rowData, userInput);
    const sortedData = sortData(filteredData, sortingColumn, sortingDirection);
    return sortedData;
  }, [rowData, userInput, sortingColumn, sortingDirection]);

With filterData looking as follows:

function filterData(rowData, userInput) {
  return rowData.filter((row) => {
    return row.name.toLowerCase().includes(userInput.toLowerCase());
  });
}

You will then only ever use the variable sortedAndFilteredData and not the rowData state.

By the way, we filter the data before sorting, so the sorting is done on the filtered data. This will result in a performance improvement, as the sorting algorithm will only be called on the filtered data.

iOS DeploymentDynamically added functions in TypeScript