redux-list-reducer
is a factory function for creating
Redux reducers that
operate on lists.
This is an early version and still a work in progress, do not use in production yet, expect the interface to change.
Read Thoughts before using this module.
The reducer works on lists, which are expected to contain unique items
among all lists. The items can be of any type as long as for any
two items a and b a !== b
holds. At the moment there are no
checks that the user doesn't include the same item twice.
There are two practical options for items:
- items are JavaScript objects
- the objects themselves are kept in a separate recuder and the items are object IDs (integer or UUID) (normalized schema)
The user is expected to handle the data in immutable fashion. Currently either JavaScript collections (default) or ImmutableJs can be used. The idea is to develop a well-tested and performant implementation of basic actions with lists.
Exported action creators:
Action creator | Explanation |
---|---|
push(item, list) |
Add item to list |
del(item) |
Delete item if it exists on any list, otherwise return the current state |
update(item, newItem) |
Update item with newItem |
move(item, toItem, before = true) |
Move item before/after toItem (Currently if toItem is not found item just gets deleted) |
moveToList(item, list) |
Moves item to list , use this if the list is empty |
toggleProperty(property, item) |
See Properties |
See the example app under example/ and tests. Particularly, teams.js shows how the library is used.
In your reducer implementation that operates on lists of data:
import listreducer from 'redux-list-reducer'
// Define your own reducer
// Rather than exporting this directly to use in `combineReducers`, pass this
// as an argument to `listreducer`. Redux never calls this function directly,
// but listReducer first tries to match its own actions, then if none
// match it calls this function with the current state and action.
const wrappedReducer = (state, action) => {
switch (action.type) {
case LOAD:
return action.result
default:
return state
}
}
// Create the final reducer and get the default actions
const {reducer, actionCreators} = listreducer({
initialState: [],
itemsProperty: 'items',
wrappedReducer
})
export default reducer
// Define your actions (they can use actions exported by listreducer or
// be completely independed), export them and the listreducer actions
// you want to expose to the user
The state listreducer
operates on is an array list list objects.
This can either be passed as initialState
or loaded by the actions
in wrappedReducer
. The following could have been used as initialState
in the example above:
const initialState = [
{
items: [
{name: 'foo'},
{name: 'bar'},
]
},
{
items: [
{name: 'baz'},
]
}
]
The property items
was defined as itemsProperty
in when calling
listreducer
above.
To use ImmutableJS, you need pass format: 'immutable'
to listreducer, and the
state needs to be an Immutable.List
and the list objects Immutable.Map
.
To use the initialState
from above:
import Immutable from 'immutable'
const {reducer, actionCreators} = listreducer({
initialState: Immutable.fromJS(initialState),
format: 'immutable'
itemsProperty: 'items',
wrappedReducer // needs to operate on Immutable objects
})
Properties can be used to track if an item is e.g. selected or being edited.
Used properties must be passed to listreducer
as params:
const {reducer, actionCreators} = listreducer({
itemsProperty: 'items',
properties: ['selected', 'editing']
})
Checking if property is active for item: list[property].has(item)
. For example,
if we're using React and rendering a list of players in a team, showing either
<EditPlayer>
or <Player>
component based on property editing
:
<ul>
{
team.players.map((player, index) =>
team.editing.has(player) ?
<EditPlayer ... />
: <Player ... />
)
}
</ul>
Then the editing state of the player can be toggled with
this.props.toggleProperty('selected', player)
assuming the toggleProperty
action creator is bound to the component.
Install from npm as a dependency of your project:
npm install redux-list-reducer --save
If you want to hack on the project, clone the repo, build it, then use npm link
to
access it from your project:
git clone https://github.com/mattikl/redux-list-reducer
cd redux-list-reducer
npm install
npm build
npm link
cd /path/to/your/project
npm link redux-list-reducer
I started implementing drag & drop within a list and between lists, then realized that with current tools all complexity lies in the reducer implementation. This module is my attempt to abstract away that complexity and make reducer implementation simple. For simple data models it succeeds in this, as can be seen in the example app.
For larger applications the correct approach seems to be normalizing nested API objects, then placing each object in its own context reducer, and the list reducers contain IDs to these objects. flux-react-router-example is an example of this using normalizr. This can be accomplished in placing list items in a separate reducer and storing their IDs in the list reducer.
I have yet to see any reducer factory gain widespread usage. It looks like the primary design goal is to keep reducers so simple that no factories are needed. redux-crud is an example of a library that provides standard actions and reducers for Redux CRUD Applications, and you can also wrap its recuder in your own reducer.
When using a factory like this, you need to understand how it works. The question is how much of its internals you need to understand, and can you count on the internals to stay the same (semantic versioning provides guarantees that the interface won't change in a backwards incompatible manner but says nothing about the internals). The factory approach helps you to start out quick, so it may be a good tool for prototyping.
On the one hand using object identity instead of external IDs feels more correct, ot the other using IDs can make things much simpler.
I will continue developing this project and learning more, but can say nothing at the moment where this project is going. Comments and ideas welcome.
- The example app is based on react-hot-boilerplate and uses react-dnd
- Book SurviveJS - Webpack and React provides a detailed walk-through of implementing Drag and Drop much like in the example app