Creating Sortable List with React, Redux, and Reselect
Will write simple React application with only one table and make it be sortable. But will keep sortable params inside Redux state and will use reselect lib to sort the collection.
The Internet is full of examples and solutions implementation of sorting in the collection, but as it often happens, the found ready-made solution does not entirely fit, as each developer tries to solve their specific problem. So at the end of the day, you may stop searching and write everything yourself, eventually getting not only the working code but also gaining over 9000 experience to self-confidence.
Here is my solution for this task, I implemented on one of our projects. This article is unlikely to be interesting for those who are already a dab hand at React and its ecosystem. It’s for those who have been familiar with it for a while and, just like me, once been looking for similar articles.
First I’ll write a simple component and then bring the logic outside. As I need, not only the component itself to know about the state of sorting but also any other component connected to the project.
So, let's get started.
Writing a simple component
Let’s create a simple React App and render the table with some data.
// index.js
import React from "react";
import { render } from "react-dom";
import App from './App';
const renderApp = () => (
<App />
);
const root = document.getElementById("app");
render(renderApp(), root);
// App.js
import React, { Component } from "react";
import data from "./shared/data";
class App extends Component {
render() {
return <div>
<table>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Age</th>
<th>Position</th>
<th>Hiring At</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
{data.map(person => (
<tr key={person.id}>
<td>{person.firstName}</td>
<td>{person.lastName}</td>
<td>{person.age}</td>
<td>{person.position}</td>
<td>{person.hiringAt}</td>
<td>{person.salary}</td>
</tr>
))}
</tbody>
</table>
</div>;
}
}
export default App;
Let’s add the ability to sort the list by any of the fields, for example, by First Name.
To do this, in the internal state of the App component, we define the collection for rendering and the sorting parameters; add a handler function that would change this collection. We will sort by clicking on the header of the First Name column.
// App.js
...
import { orderBy } from "lodash";
import data from "./shared/data";
...
constructor(props) {
super(props);
this.state = {
collection: data,
sortParams: {
direction: undefined
}
};
}
handleColumnHeaderClick() {
const {
collection,
sortParams: { direction }
} = this.state;
// Check, what direction now should be
const sortDirection = direction === "desc" ? "asc" : "desc";
// Sort collection
const sortedCollection = orderBy(
collection,
["firstName"],
[sortDirection]
);
//Update component state with new data
this.setState({
collection: sortedCollection,
sortParams: {
direction: sortDirection
}
});
}
...
<th onClick={() => this.handleColumnHeaderClick()}>First Name</th>
...
I use the orderBy function because inside the project we widely use lodash, and it is just more convenient here than the native implementation.
Then we can complicate it and make a general sorting function by bringing in the field by which we want to sort:
// App.js
...
handleColumnHeaderClick(sortKey) {
...
const sortedCollection = orderBy(collection, [sortKey], [sortDirection]);
...
}
...
<th onClick={() => this.handleColumnHeaderClick("firstName")}>
First Name
</th>
<th onClick={() => this.handleColumnHeaderClick("lastName")}>
Last Name
</th>
<th onClick={() => this.handleColumnHeaderClick("hiringAt")}>
Hired At
</th>
...
Now our component is able to sort the collection in almost any field.
Why almost?
At the moment we don’t pay much attention to the field type and sort by it as if it were a string or a number. But there is a date in our collection. And sorting by such a field will not work correctly since the data in the object is represented by a string. But, we will solve this “problem” a bit later 🙂.
Adding Redux and Reselect, made an App more complex
Our component perfectly solves the task. But, what if some other part of our App needs to be aware of how our list is now sorted? For example, you need to show outside the list component that it is sorted - to render some marker or icon, or you generally sort your list on the server, and then you need to transfer the sorting parameters to it. And here, obviously, we require that the state in which the parameters are stored to be external.
To do this, we add Redux to our App and put our collection in the initialState of the future store.
// index.js
import React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { configureStore } from "./store/configureStore";
import App from "./App";
import data from "./shared/data";
const initialState = {
app: {
developersList: data
}
};
const store = configureStore(initialState);
const renderApp = () => <Provider store={store}><App /></Provider>;
const root = document.getElementById("app");
render(renderApp(), root);
Let’s describe reducers:
// ./reducers/index.js
import { combineReducers } from "redux";
import app from "./app";
export default combineReducers({
app
});
// ./reducers/app.js
import * as actionTypes from "../actionTypes";
const initialState = {};
export default (state = initialState, action) => {
switch (action.type) {
case actionTypes.SET_SORT_PARAMS:
return { ...state, sortParams: action.payload.data };
default:
return state;
}
};
Now let’s add actions:
import { get } from "lodash";
import * as types from "../actionTypes";
export function setSortParams(sortKey, sortType) {
return (dispatch, getState) => {
const { sortParams } = getState().app;
const order = get(sortParams, "order");
dispatch({
type: types.SET_SORT_PARAMS,
payload: {
data: {
key: sortKey,
order: order === "desc" ? "asc" : "desc",
type: sortType
}
}
});
};
}
In orderByType, we can transfer not only the key by which the collection will be sorted but also the data type, to clarify exactly how it will be sorted.
So finally the code of the main component has turned into:
// app.js
import React from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { compose } from "recompose";
import * as actions from "./actions";
function App(props) {
const { collection, setSortParams, sortParams } = props;
console.log("sortParams", sortParams);
return (
<div>
<table>
<thead>
<tr>
<th onClick={() => setSortParams("firstName")}>First Name</th>
<th onClick={() => setSortParams("lastName")}>Last Name</th>
<th onClick={() => setSortParams("age")}>Age</th>
<th onClick={() => setSortParams("position")}>Position</th>
<th onClick={() => setSortParams("hiringAt", "date")}>Hired At</th>
<th onClick={() => setSortParams("salary")}>Salary</th>
</tr>
</thead>
<tbody>
{collection.map(person => (
<tr key={person.id}>
<td>{person.firstName}</td>
<td>{person.lastName}</td>
<td>{person.age}</td>
<td>{person.position}</td>
<td>{person.hiringAt}</td>
<td>{person.salary}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
const mapStateToProps = (state, props) => ({
collection: state.app.developersList,
sortParams: state.app.sortParams
});
const mapDispatchToProps = dispatch => ({
setSortParams: bindActionCreators(actions.setSortParams, dispatch)
});
const enhance = compose(
connect(
mapStateToProps,
mapDispatchToProps
)
);
export default enhance(App);
Now clicking on the table headers, we write the sorting parameters into redux state. And we can get them from any component subscribed to the state (precisely what we did in our component).
But now no sorting happens, because the component is transferred as props
collection: state.app.developersList
and it still doesn’t depend on parameters changing in the state. Let’s fix it.
We could leave the handleColumnHeaderClick function inside the main component and sort the collection in the same place. But we want our component to know nothing about which collection and how it renders.
Using library reselect, we will write a selector for our collection
// ./selectors/index.js
import { createSelector } from "reselect";
import get from "lodash/get";
import orderBy from "lodash/orderBy";
import moment from "moment";
const developersSelector = state => state.app && state.app.developersList;
export const sortSelector = state => state.app && state.app.sortParams;
function orderByType(data, type) {
switch (type) {
case "date":
return Date.parse(data);
default:
return data;
}
}
export const getSortedDevelopersCollection = createSelector(
developersSelector,
sortSelector,
(developersCollection, sort) => {
if (sort) {
return orderBy(
developersCollection,
c => orderByType(get(c, sort.key), sort.type),
[sort.order || "desc"]
);
}
return developersCollection;
}
);
And we’ll subscribe our component to state using a selector:
// index.js
...
import { getSortedDevelopersCollection, sortSelector } from "./selectors";
...
const mapStateToProps = (state, props) => ({
collection: getSortedDevelopersCollection(state),
sortParams: sortSelector(state)
});
...
Along with the selector, we wrote one helper function – orderByType. We can set how exactly the collection will be sorted via this function. For example, above, in setSortParams for the hiringAt field, we passed the date type as the second argument. And during the sorting, instead of a string in the “mm/dd/yyyy” format, we cast the date values to Date type and thereby solve the sorting mentioned above by date problem.
In orderByType we also may add a case with a float or number type:
case "float":
return parseFloat(data);
case "number":
return parseInt(data, 10);
This can be useful if the data in the collection does not match its type. For example, the age or amount of salary is given as a string. And then setting
setSortParams("salary", "float")
the data will be cast to the correct type.
That’s it! Now the component is subscribed to the state, where the collection comes from, but already rendered by the parameter-aware selector function. The only thing that remains inside the component is the action call with the transfer of a key and possibly a type to it.
What We’ve Done?
We’ve turned the App component out of the complex, and in the long run hard to expand and hard to maintain into an almost “clean” one and brought out all the logic that implements the sorting into its proper place:
The code in action is responsible for handling the click on the appropriate header and calling the reducer
Reducer puts data into a global state
Selector code is in charge of selecting data from the state and their sorting, depending on the parameters that came from the reducer
In this case, it is possible to change the behavior of the sorting function or the selector as much as you want - the component that renders the collection doesn’t need to be changed at all.
Source Code
All source code is available on CodeSandbox with additional options.
“Clear Sort” button
Sort direction arrows
More styling
Get fun! Use React!