简体   繁体   中英

How to sort an array of complex objects?

I have this method for sorting an array of objects, but when I have a complex structure, for example:

const data = [ { title: 'Book title', author: 'John', info: { language: english, pages: 500, price: '$50' }} ]

I can't sort the second level of the object 'info: {language: english, pages: 500, price:' $ 50 '}'

My Code:

import { useMemo, useState } from 'react';

interface SortConfigProps {
  key: string;
  direction: string;
}

export const useSortableData = <T>(items: T[]) => {
  const [sortConfig, setSortConfig] = useState<SortConfigProps>(
    {} as SortConfigProps,
  );

  const sortedItems = useMemo(() => {
    const sortableItems = [...items];

    if (sortConfig) {
      sortableItems.sort((a: any, b: any) => {
        if (a[sortConfig.key] < b[sortConfig.key]) {
          return sortConfig.direction === 'ascending' ? -1 : 1;
        }
        if (a[sortConfig.key] > b[sortConfig.key]) {
          return sortConfig.direction === 'ascending' ? 1 : -1;
        }
        return 0;
      });
    }
    return sortableItems;
  }, [items, sortConfig]);

  const requestSort = (key: string) => {
    let direction = 'ascending';
    if (
      sortConfig &&
      sortConfig.key === key &&
      sortConfig.direction === 'ascending'
    ) {
      direction = 'descending';
    }
    setSortConfig({ key, direction });
  };

  return { items: sortedItems, requestSort, sortConfig };
};

The problem you are running into is deep cloning of an object. the spread operator only goes 1 level deep and you are trying to go two. You can use libraries from lodash or other deep cloning. I use the JSON trick.

     const [data, setData] = useState(initData);
     function newArray() {
        return JSON.parse(JSON.stringify(data));
      }

Example shows sorting with two levels: https://codesandbox.io/s/serverless-sea-jt220?file=/src/App.js

There are multiple ways to implement this based on your overall system so for my suggestion, I am going to assume the following:

  • You want to keep the data as is for performance or some other reasons (ie flattening the object through some kind of "normalizer" is not an option).
  • The key cannot be an object, but has to be a string
  • Either you or the user can set the key.
  • There exists a character or a set of characters that can be used in a key string as a delimiter to construct a tree of keys (eg a dot in 'info.price' , or an arrow in 'info->price' ). An important property of the delimiter is that it is not valid to use it in a flat key (ie in the last example something like data = [{ 'info->price': '$50' }] is not allowed)

Ok now you just need to implement an accessor to use the complex keys on your object, something similar to Lodash.get . A simple implementation would be something like:

const DELIMITER = '->';

function get(obj, key) {
  if (!obj) {
    // in case of null or undefined
    return obj;
  }
  if (!key) {
    // empty string or something like that
    return obj;
  }
  if (key.includes(DELIMITER)) {
    const keyComponents = key.split(DELIMITER);
    const firstKeyComponent = keyComponents.shift();
    const newKey = keyComponents.join(DELIMITER);
    return get(obj[firstKeyComponent], newKey)
  }
  return obj[key];
}

Emphasis on the simple here, because recalculating keyComponents every time is not ideal. You also might want add extra if cases for how to handle strings or arrays, those could cause problems if a key component is a number.

EDIT: Also maybe use Object.prototype.hasOwnProperty to check if a key is identical to a built in Object function, or better yet check if obj is a function and decide how to handle that scenario.

After you have this you can just replace that compare segment of your code with this:

  sortableItems.sort((a: any, b: any) => {
    const aValue = get(a, sortConfig.key);
    const bValue = get(b, sortConfig.key);
    if (aValue < bValue) {
      return sortConfig.direction === 'ascending' ? -1 : 1;
    }
    if (aValue] > bValue) {
      return sortConfig.direction === 'ascending' ? 1 : -1;
    }
    return 0;
  });

And you're good to go for most cases. I don't know how "wild" your data can get so make sure to test a bunch of scenarios.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM