A higher order Redux reducer (or transducer) that adds pagination, sorting, and filtering to a reducer of an array of objects.
It is distributed as an ES5 CommonJS module.
npm install paginated-redux --save-dev
import paginated from 'paginated-redux'
or
const paginated = require('paginated-redux')
This is a "higher order" reducer. That is, it modifies an existing reducer and extends it with new functionality. It assumes the base reducer's state is simply an array of objects. It then acts on that array providing the ability to paginate it, sort it (on a user-definable property of each object in the array), and filter the array with a string, matching it against the content of the properties of the objects in the array.
To set it up, let's first assume you have a simple reducer that is an array of user objects. I'm also assuming, here, that you have some action-constants defined in a separate file called 'actionTypes.js'.
import paginated from 'paginated-redux';
import {
STORE_USERS,
ADD_USER,
UPDATE_USER,
REMOVE_USER,
GOTO_USERS_PAGE,
NEXT_USERS_PAGE,
PREV_USERS_PAGE,
SORT_USERS,
FILTER_USERS
} from 'actionTypes';
const updatedUser = user => {
// ...update user object
return user;
};
// Define base users reducer along with basic user actions.
const users = (state = [], action) => {
switch (action.type) {
case STORE_USERS:
return action.users;
case ADD_USER:
return [...state, action.user];
case UPDATE_USER:
return state.map(updatedUser(action.user));
case REMOVE_USER:
return state.filter(user => (user.id !== action.id));
default:
return state;
}
};
// Extend the users reducer with paginated actions, and pass the user-specific
// pagination actions to the paginated transducer (this way, if you have
// multiple paginated-extended reducers, you can keep each paginated array's
// action separated).
const paginatedUsers = paginated(users, {
GOTO_PAGE: GOTO_USER_PAGE,
NEXT_PAGE: NEXT_USER_PAGE,
PREV_PAGE: PREV_USER_PAGE,
FILTER: FILTER_USERS,
SORT: SORT_USERS
});
// Finally, export the newly extended reducer instead of the base reducer.
export default paginatedUsers;
The new extended reducer will now have a modified state. The original array of
user objects will now be stored in a property called list
, and some new
pagination-oriented properties will also be added which you can use for
rendering your UI (e.g., what page you're on, total pages, current filter,
etc.).
This is the total, original, array from the base reducer. When you modify the base array (e.g., by adding a new object, removing one, etc.) this array will be updated.
This is an array of objects which represents the current page.
This is mostly used internally, but represents all items from
list
, but filtered against filter
which is what's actually used to calculate
the pageList
as the pages correspond to what's left over in the cacheList
.
The current page number (starting from 1).
The total pages currently available based on cacheList
.
The number of elements that are used for each page.
The current sort order (either "asc" or "desc").
The current sorting property. For example you could sort an array of user
objects by a property "name" (based on the order
) or change this to instead
sort by their "lastModified" property (if those are properties you have in your
user object).
There are new actions you can execute, and define with reducer-specific names, which will act on your array of objects in certain ways, as well as maintain the current pagination state.
Note that in the example action creators, I'm using user-specific action names
since these are what I passed into the paginated transducer to act on this user
reducer (e.g., GOTO_PAGE: GOTO_USER_PAGE
).
Go to a specific page defined in the action. For example, to go to page 5, you might write an action creator that looks like this:
export const gotoUserPage = page => ({
type: GOTO_USER_PAGE,
page
});
Go to the next page in list. If at the last page, go to the first
page (wraps back around). If you don't want to have this wrapping effect, simply
check, in your UI, if the current page
in the state is equal to the total
and either disable or remove the "next" button in your UI.
export const nextUserPage = () => ({
type: NEXT_USER_PAGE
});
Go to the previous page in the list. If on the first page, go to
the last page (wraps back around). If you don't want to have this wrapping
effect, simply check, in your UI, if the current page
in the state is equal to
1 and either disable or remove the "previous" button in your UI.
export const prevUserPage = () => ({
type: PREV_USER_PAGE
});
Sort the current paginated list by the property defined in by
. If the
current value of by
in the state is equal to the value passed into the SORT
action, simply toggle the order (reverse the order that it is currently in). If
the value of by
in the state is different than the value passed into the
SORT
action, then re-order the list based on the new value in 'asc' order.
export const sortUsers = by => ({
type: SORT_USERS,
by
});
Filter the list down by matching any property of the objects in the
array against the value of filter
in the state. This changes the cacheList
by creating a new array with all matching objects from list
that correspond to
filter
.
export const filterUsers = filter => ({
type: FILTER_USERS,
filter
});
You can specify some default values for the paginated reducer during creation by
passing a third argument to paginated
with a set of options defined as
follows.
const paginatedUsers = paginated(users, {
GOTO_PAGE: GOTO_USER_PAGE,
NEXT_PAGE: NEXT_USER_PAGE,
PREV_PAGE: PREV_USER_PAGE,
SORT: SORT_USERS,
FILTER: FILTER_USERS
}, {
defaultPage = 1,
defaultSortOrder = 'asc',
defaultSortBy = 'name',
defaultPer = 10,
defaultFilter = '',
defaultTotal = 0
})
So, for example, if you want each of your pages to contain 20 objects instead of
10, simply define defaultPer
with a value of 20. Note that these are simply
the initial values for these properties. You can change them later in your
program by setting them directly. When you call the paginated actions, the
values of these properties do change ;)
This is a higher order reducer that works with a Redux reducer, so it doesn't really make sense outside that context.
It also depends on the base reducer being simply an array of objects (not a complex object itself).
- add ability to define sort order when calling the action
- add ability to filter based on custom criteria (rather than every property)
- possibly rename
cacheList
tofilteredList
as it is more meaningful - maybe also rename
pageList
topagedList
to indicate that it is derived from the originallist
- add ability to use a "flat" array of values rather than objects and still use all the same functionality
MIT