Where should I handle sorting in Redux App?

22,482

Solution 1

I save the items, sortKey and sortKind (asc/desc) in the Redux Store.

In my Angular component (I believe would be same for React), I get the store state as an Observable so that I can display the items, sortKey and sortOrder in the UX.

When the user clicks on the table columns to change sort key (order) I dispatch the new keys/sort order to reducer for the state.

The reducer then performs the new sorting, and returns the new state with the updated values.

The Observable in the component thus sparks an event which updates the UX.

Advantage:

  • keep sorting logic out of the component

  • by saving the sortKey and sortKind in the state, you can restore precisely the UX if the user refreshes the browser (I use Redux-LocalStorage to sync)

  • as the store has the sorted items, you'll only perform sorting when the user actively wants it.

  • the sorted items are remembered for when the user might return to the component.

My reducer ( "bizzes" is my items list, and I use Immutable.List to store the items)

import { List }                     from 'immutable';
import { IBizz, IBizzState }   from './bizz.types';
import { BIZZES_SET, BIZZES_SORT}    from 'store/constants';

const SORT_ASC = 'asc';
const SORT_DESC = 'desc';

const defaultSortKey = 'serialNo';
const defaultSortOrder = SORT_ASC;

const INITIAL_STATE: IBizzState =  {
    bizzes: List([]),
    sortKey: defaultSortKey,
    sortOrder: defaultSortOrder
};

export function bizzReducer(state: IBizzState = INITIAL_STATE, action: any): IBizzState {

    switch (action.type) {

        case BIZZES_SET:
            return {
                bizzes: List(action.payload.bizzes),
                sortKey: action.payload.sortKey || defaultSortKey,
                sortOrder: action.payload.sortOrder || defaultSortOrder
            };

        case BIZZES_SORT:
            let sortKey = action.payload.sortKey || defaultSortKey;

            if(sortKey === state.sortKey) {
                state.sortOrder = state.sortOrder === SORT_ASC ? SORT_DESC : SORT_ASC;
            }

            return {
                bizzes: List(state.bizzes.sort( (a, b) => { 
                    if( a[sortKey] < b[sortKey] ) return state.sortOrder === SORT_ASC ? -1 : 1;
                    if( a[sortKey] > b[sortKey] ) return state.sortOrder === SORT_ASC ? 1: -1;
                    return 0;
                })),
                sortKey: sortKey,
                sortOrder: state.sortOrder
            };
        default: return state;
    }
}

And my component ( I use Ng2-Redux to get the store as Observables):

import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { select } from 'store';
import { BizzActions } from 'actions/index';

@Component({
    selector: 'bizzlist',
    templateUrl: './bizz-list.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BizzListComponent implements OnInit {


    @select([ 'bizzState']) bizzState$;

    public sortOrder: string;
    public sortKey: string;
    public bizzes = [];
    private bizzStateSubscription; 


    constructor( 
        public bizzActions: BizzActions
    ) { }

    ngOnInit() {
        this.bizzStateSubscription = this.bizzState$.subscribe( bizzState => {
            this.bizzes = bizzState.bizzes;
            this.sortKey = bizzState.sortKey;
            this.sortOrder = bizzState.sortOrder;
        });
     }

    ngOnDestroy() {
        this.bizzStateSubscription.unsubscribe();
    }



    public sortBizzes(key) {
        this.bizzActions.sortBizzes(key); 
    }
}

As you can see, I am using an Action (called BizzActions) to do the actual Redux dispatch. You could do it in your component, but I prefer to separate these things. For good measure, here's my BizzActions (a Service):

import { Injectable }           from '@angular/core';
import { NgRedux, IAppState }   from 'store';
import { 
    BIZZES_SET,
    BIZZES_SORT 
} from 'store/constants';

@Injectable()
export class BizzActions {

    constructor (private ngRedux: NgRedux<IAppState>) {}

    public setBizzes = (bizzes: any) => {
        return this.ngRedux.dispatch({
            type: BIZZES_SET,
            payload: {
                bizzes: bizzes
            }
        });
    };

    public sortBizzes = (key:string) => {
        return this.ngRedux.dispatch({
            type: BIZZES_SORT,
            payload: {
                sortKey: key
            }
        });
    };

}

Solution 2

IMO, the right place to sort data is not directly in the reducers but in the selectors.

From redux docs:

Computing Derived Data

Reselect is a simple library for creating memoized, composable selector functions. Reselect selectors can be used to efficiently compute derived data from the Redux store.

I'm currently using selectors to filter and sort data.

  1. No data repetition in the state. You don't have to store a copy of the item sorted by one specific way.
  2. The same data can be used in different components, each one using a different selector function to sort for example.
  3. You can combine selector applying many data computations using selector that you already have in the application.
  4. If you do right, your selectors will be pure functions, then you can easily test them.
  5. Use the same selector in many components.

Solution 3

You could sort the data when @connect -ing your React component with the Redux store:

function mapStateToProps(state) {
   var items = state.items.slice(0);
   items.sort()
   return {
     items: items
   }
}

@connect(mapStoreToProps)
class MyComponent extends React.Component {
   render() {
      var items = this.props.items;
   }
}

The Redux documentation shows a similar case in the Todo example: https://redux.js.org/basics/usage-with-react

Solution 4

I've been sorting my reducers using a section dictionary pattern. In other words, I sort my items by headers, say a date, and then store the objects in arrays by the date key:

sectionHeaders: ["Monday", "Tuesday"],
dict:{
    Monday: [{obj1},{obj2},{obj3}],
    Tuesday: [{obj4}],
}

Then I use this dict in React Native to populate my ListView because ListView will except this object format to render Items with Sections using the cloneWithRowsAndSections method.

This is a performance optimization because my sorting is not trivial. I have to make deep comparisons and this way I only do it once when I first populate the store, and not every time I render the scene.

I've also played around with using a dictionary by ID and storing only the IDs in the sorted dict instead of the actual objects.

There are trade offs though for this, in that updating is more complex and you have to decide when to remove section headers if an item is removed from a section.

Share:
22,482
ZPPP
Author by

ZPPP

Updated on January 20, 2020

Comments

  • ZPPP
    ZPPP over 4 years

    I have an action / reducer / components. In one of my components (component dump) I have a Select. I get information on what type of filter my store. Where can I handle it in action, or reducer?

  • Denis
    Denis almost 8 years
    It's easy and neat solution but it will be executed on every state change :\
  • Michael Younkin
    Michael Younkin almost 8 years
    You can avoid sorting on every state change by only recomputing the selector result when it's arguments from the state actually change. There are a few libraries out there that can do this, and it's pretty easy to write your own as well as long as you're using redux correctly (ie. your reducers are pure). Take a look at reselect.
  • tech4242
    tech4242 about 7 years
    Yes, Selectors are definitely an elegant solution
  • totymedli
    totymedli about 6 years
    Watch out when you copy-paste from the docs! It can copy invisible characters that break the build by throwing errors like this: Module build failed: SyntaxError: Unexpected character '​',