How to Sort List of React Components Based on Different Properties?
So, This Is a Code Snippet from My Frontend. {Store. Results. Data. Map( Result => )} Basically, I'm Mapping All the Data in My Redux Store into Resultitems. I...
So, this is a code snippet from my frontend.
{store.results.data.map( result =>
<ResultItem
key={result.id}
title={result.title}
description={result.description}
start_date={result.start_date}
end_date={result.end_date}
vendor_name={result.vendor.name}
buyer_name={result.buyer.name}
preview_file={result.preview_file}
status={result.status}
/>
)}
Basically, I'm mapping all the data in my Redux store into ResultItems. I want to be able to sort all my ResultItems by different properties like title, description, start_date, end_date, vendor_name, and buyer_name.
Any ideas on how to do that?
6 Answers
I would create a Sort component and wrap the result items with it like this:
<Sort by='title'>
{store.results.data.map( result =>
<ResultItem
key={result.id}
title={result.title}
description={result.description}
start_date={result.start_date}
end_date={result.end_date}
vendor_name={result.vendor.name}
buyer_name={result.buyer.name}
preview_file={result.preview_file}
status={result.status}
/>
)}
</Sort>
// Note: The 'by' prop passed into <Sort> in my example is hardcoded by you can grab it from redux or set it when user selects something from a list. Also you can pass another prop to indicate whether to sort by ascending or descending order etc
Then within the Sort component you can access the array of result items like this React.Children.toArray and use the sort method for arrays and pass the sort method a 'compare' function.
// Sort.js
import React from 'react';
// Compare function needed by the Sort component
const compare =(a, b) => {
// you can access the relevant property like this a.props[by]
// depending whether you are sorting by tilte or year, you can write a compare function here,
}
const Sort= ({children, by})=> {
If (!by) {
// If no 'sort by property' provided, return original list
return children
}
return React.Children.toArray(children).sort(compare)
}
The main advantage with above is that you can use the Sort component anywhere else and you can keep the sorting logic neatly separate from mapping over the initial results.
You have to sort the data before doing the map part. For example imagine you want to sort by id:
{store.results.data.sort((a, b) => a.id - b.id).map( result =>
<ResultItem key={result.id}
title={result.title}
description={result.description}
start_date={result.start_date}
end_date={result.end_date}
vendor_name={result.vendor.name}
buyer_name={result.buyer.name}
preview_file={result.preview_file}
status={result.status}
/>)}
First of all, you should know that is is nearly impossible to process react components - this includes sorting them as well.
Secondly, if you have an array (say arr) and you call .sort() method on it, the array will get sorted in-place, i.e., arr will get modified.
Now, time for your question. As you point out here, you want to implement dynamic sorting. This requires some custom compareFunctions that know how to compare objects based on their keys. Here's an example:
arr = [
{
num: 1,
text: 'z',
},
{
num: 2,
text: 'y'
},
{
num: 3,
text: 'x'
},
];
const ASC = 'ascending';
const DSC = 'descending';
function sortByNum(a, b, order = ASC) {
const diff = a.num - b.num;
if (order === ASC) {
return diff;
}
return -1 * diff;
}
function sortByText(a, b, order = ASC) {
const diff = a.text.toLowerCase().localeCompare(b.text.toLowerCase());
if (order === ASC) {
return diff;
}
return -1 * diff;
}
console.log(arr.sort((a, b) => sortByNum(a, b, DSC)))
console.log(arr.sort((a, b) => sortByText(a, b, ASC)))
Here's how your code might look:
const ASC = 'ascending';
const DSC = 'descending';
function sortByTitle(a, b, order = ASC) {
...
}
function sortByStatus(a, b, order = ASC) {
...
}
function render() {
let sortChoice = ... // from `props` perhaps?
const data = store.results.data;
switch (sortChoice) {
case 'title':
data.sort(sortByTitle);
break;
case 'status':
data.sort(sortByStatus);
break;
...
}
return {data.map(result =>
<ResultItem
key={result.id}
title={result.title}
description={result.description}
start_date={result.start_date}
end_date={result.end_date}
vendor_name={result.vendor.name}
buyer_name={result.buyer.name}
preview_file={result.preview_file}
status={result.status}
/>
)}
}
Lodash library can serve this purpose.
{_.sortBy(store.results.data, ['status', 'start_date']).map( result =>
<ResultItem key={result.id}
title={result.title}
description={result.description}
start_date={result.start_date}
end_date={result.end_date}
vendor_name={result.vendor.name}
buyer_name={result.buyer.name}
preview_file={result.preview_file}
status={result.status}
/>)}
You can provide multiple fields in ['status', 'start_date',...]by which you want to sort.
If you are using a select to choose which field you want to sort you can create something like this:
<select onChange={(e) => {
props.dispatch(sortMyArray(e.target.value))
}}>
<option value="start_date">Start Date</option>
<option value="title">Title</option>
<option value="description">description</option>
</select>
and in your sort method sortMyArray you can do :
export default sortMyArray(sortBy){
return myArray.sort((a, b) => {
if (sortBy === 'start_date') {
return a.start_date < b.start_date ? 1 : -1;
}
});
};
I got this working with this component code:
/* eslint-disable react/jsx-no-useless-fragment */
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import './Sort.module.scss';
export interface SortProps {
children: React.ReactNode;
childType: string;
by?: any;
keyWith?: string;
}
// thx:
// thx:
// -------------------
export const Sort: React.FC<SortProps> = ({ children, childType, by, keyWith = "id" }) => {
// ##################################################################################
// # COMPARISON FUNCTION
// ##################################################################################
const compare = (aRaw: any, bRaw: any): any => {
const a = aRaw.props[childType];
const b = bRaw.props[childType];
// Compare function needed by the Sort component
// you can access the relevant property like this a.props[by]
// depending whether you are sorting by tilte or year, you can write a compare function here,
if (!!a && !!b) {
switch (by) {
case "createdAt":
return b[by] > a[by] ? 1 : -1;
break;
case "description":
return a[by].localeCompare(b[by]);
break;
}
}
}
// ##################################################################################
// # /end COMPARISON FN
// ##################################################################################
if (!children) {
return (<></>);
}
if (!by) {
// If no 'sort by property' provided, return original list
return (<>{children}</>);
} else {
// trying to workaround error: Each child in a list should have a unique "key" prop
const elements: any[] = React.Children.toArray(children);
const keyed: any[] = elements
.sort(compare)
.map((child, newIdx) => {
return React.cloneElement(child, { key: (child as any)[keyWith], idx: newIdx });
});
return (<>{keyed}</>);
}
}
export default Sort;
Which then I use as follows (storybook stories):
import React from 'react';
import Sort from './Sort';
type CartModified = {
description: string;
id: number;
name: string;
createdAt: string;
}
export default {
component: Sort,
title: 'Sort',
};
const carts = [
{ id: 2, name: "2nd", createdAt: "2021-04-23T23:27:51.943Z", description: "d" },
{ id: 3, name: "3rd", createdAt: "2021-05-23T23:27:51.943Z", description: "b" },
{ id: 1, name: "1st", createdAt: "2021-03-23T23:27:51.943Z", description: "c" },
{ id: 4, name: "4th", createdAt: "2021-06-23T23:27:51.943Z", description: "a" },
] as CartModified[];
const TestItem: React.FC<{ c: CartModified }> = ({ c }) => {
return <div>
{c.name} # {c.id} - {c.createdAt} ({c.description})
</div>;
}
// -------------------
export const withSortByCreatedAt = () => {
return <Sort by="createdAt" childType="c" >
{carts.map((c) => !c ? null : (
<TestItem
key={c.id}
c={c}/>
))}
</Sort>;
};
// -------------------
export const withSortByCategory = () => {
return <Sort by="description" childType="c" >
{carts.map((c) => !c ? null : (
<TestItem
key={c.id}
c={c}/>
))}
</Sort>;
};