redux-normal-reducer
v0.3.0
Published
This package attempts to reduce some of the boilerplate involved in managing [normalized state](https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape) in redux. It attempts to define a common set of action types for manipulating this s
Downloads
2
Readme
Intro
This package attempts to reduce some of the boilerplate involved in managing normalized state in redux. It attempts to define a common set of action types for manipulating this state, such as by adding, updated, or removing items from it. It does not (yet) manage relationships between items.
It's designed to produce a reducer to handle these action types, as well as creators for these actions. If you want to have more control over the reducer, you can compose your own using the lower-level reducers in this package.
The redux philosophy is followed: where an individual item is modified, an
entirely new object is produced. Where it makes sense, the byId
and
allIds
items in the state are copied as well, so that the ===
operator
can detect the change, and the resulting state object is also a copy. When
an action won't result in a change, the original state is returned. Anything
in the state besides allIds
and byId
is not modified.
Installation
Install as a dependency:
npm i --save redux-normal-reducer
Or clone it and work with it locally.
This project was originally created with pnpm but there's no reason you can't use npm instead. Some npm scripts are available:
# Build it
npm run build
# Generate JSDocs
npm run docs
# Test it
npm start
This package contains Jest unit tests, and seems to have full code coverage.
Redux Actions
This package is opinionated about the structure of the actions it handles. Here are some examples:
// General data action, contains a single item
{
type: "ADD",
data: {
id: "foo",
name: "John"
// Other items...
}
}
// Array action, contains multiple items, type usually contains "ALL"
{
type: "ADD_ALL",
data: [{
id: "foo",
name: "John"
}, {
id: "bar",
name: "Jane"
}]
}
// Remove action, contains an ID
{
type: "REMOVE",
id: "foo"
}
// Remove many action, contains several IDs
{
type: "REMOVE_ALL",
ids: ["foo", "bar"]
}
// Move action, contains a source and destination ID
{
type: "MOVE",
from: "foo",
to: "bar"
}
The value for type
is somewhat flexible. If you're composing a reducer using the
basic reducers in the this package, type
is ignored. If you're using the reducer
or actions
factories, you can add a prefix or suffix to the action types to
customize them. Those customizations will be reflected in the action creators as well.
Configuring Actions
Redux encourages the use of string action types, and mapping those action types onto
constants. In addition, creating the actions is generally expected to be done using
functions, especially when using redux with React as with
React Redux. You can use the actions
method to take
care of creating these action types in a regular way, and optionally customize the
types using a prefix or suffix (or both).
The prefix or suffix options covers two use cases. If you want the action names to
reflect the type of data you're manipulating (e.g. a user
or todoItem
), you
could use a suffix for natural semantics (e.g. ADD_USER
or REMOVE_USER
) or a
prefix so that actions for a particular data type get grouped together
alphabetically (e.g. TODO_ADD
, TODO_REMOVE
, USER_ADD
, USER_REMOVE
).
The output of the actions
method is designed to be easily destructured onto
constants in your module.
import { actions } from 'redux-normal-reducer'
const mapping = actions({ prefix: 'USER_' })
// Destructure only what you need...
export const { USER_ADD, USER_REMOVE, USER_MERGE } = mapping.types
export const { userAdd, userRemove, userMerge } = mapping.creators
// Feel free to define your own actions...
export const SORT_USER = 'SORT_USER'
export const sortUser = (key, direction) =>
({ type: SORT_USER, key, direction })
High-Level Reducer Factories
Use the reducer
and throwingReducer
methods to create Redux reducers that handle
all of the basic action types. throwingReducer
is a customized version of reducer
,
with the unsafe reducers replaced with throwing reducers. Since they raise exceptions,
they should be used with care in production applications, since an uncaught exception
can terminate the application.
Each factory accepts a configuration that may (optionally) contain an action type
prefix, a suffx, and a customized mapping for any action types where you wish to
customize the reducer. The resulting reducer can be used with redux, e.g. using
combineReducers
.
import { reducer, throwingReducer, mergeReducer } from 'redux-normal-reducer'
// Plain reducer, contains all basic actions
const users = reducer()
// Customize the actions with a prefix, and use a custom merge
const merger = ( existing, update ) => ({
...existing,
...update,
updatedOn: new Date().toISOString()
})
const users = reducer({
prefix: 'USERS_',
customReducers: {
USERS_MERGE: mergeReducer(merger)
},
defaultState: {
allIds: [],
byId: {},
otherState: 1
}
})
// Use a throwing reducer in development
let users
if (process.env.NODE_ENV === 'devlopment') {
users = throwingReducer()
} else {
users = reducer()
}
Normalizr Data
One package for handling related data and flattening the results is
Normalizr. Given a nested data
structure, where individual items may contain relationships to other items,
normalizr
will flatten the data, keeping the actual objects in one place and
maintaining relationships in the form of IDs or lists of IDs. Example:
import { normalize, schema } from 'normalizr';
// Data such as might be retrieved from an API
let data = {
"id": "123",
"author": {
"id": "1",
"name": "Paul"
},
"title": "My awesome blog post",
"comments": [
{
"id": "324",
"commenter": {
"id": "2",
"name": "Nicole"
}
}
]
}
// -- Define schemas containing relationships --
// Define a users schema
const user = new schema.Entity('users');
// Define your comments schema
const comment = new schema.Entity('comments', {
commenter: user
});
// Define your article
const article = new schema.Entity('articles', {
author: user,
comments: [comment]
});
const normalizedData = normalize(originalData, article);
// Normalized data looks like this:
{
result: "123",
entities: {
"articles": {
"123": {
id: "123",
author: "1",
title: "My awesome blog post",
comments: [ "324" ]
}
},
"users": {
"1": { "id": "1", "name": "Paul" },
"2": { "id": "2", "name": "Nicole" }
},
"comments": {
"324": { id: "324", "commenter": "2" }
}
}
}
You can create a reducer capable of handling the results of normalizr.normalize
.
Note that normalizr
is not a dependency of redux-normal-reducer
, so you have to
take care of creating the schemas and normalizing your data. This reducers are
created using the following factory:
import {
updateNormalizedReducer,
addOrMergeAllReducer,
reducer
} from 'redux-normal-reducer'
// Create reducers to handle artices and users
// The key used with updateNormalizedReducer must match the
// key under "entities" in the normalized data.
const updateNormalizedArticles = updateNormalizedReducer('articles')
const updateNormalizedUsers = updateNormalizedReducer('users')
// The default method for handling the data uses addOrMergeAll
let state = { users: { byId: {}, allIds: [] }, articles: { byId: {}, allIds: [] } }
state.articles = updateNormalizedArticles(state.articles, { data: normalizedData })
state.users = updateNormalizedArticles(state.users, { data: normalizedData })
// You can customize it to use, for example a custom merge implementation
const updateNormalizedArticles = updateNormalizedReducer(
'articles',
addOrMergeAllReducer(myCustomMergeFunction)
)
// And integrate it into your high-level reducer factory:
const users = reducer({suffix: '_USER', customReducers: {
UPDATE_NORMALIZED_USER: updateNormalizedReducer('users')
}})
The UPDATE_NORMALIZED
action is supported by the actions method
described above.
Low-Level Reducers
The following reducers are available, if you wish to compose your own reducer. Each one
handles a single action type. They ignore the type
key in the action, and simply
execute the state manipulation. All reducers have the signature:
function (state, action) {}
Unchecked Reducers
These reducers sacrifice safety for performance, and don't check the inputs to make
sure they won't result in inconsistent state (e.g. allIds
doesn't reflect everything
that's in byId
).
add
(data action): adds an item to the stateaddAll
(array action): adds items to the statecreate
(data action): creates an item in the state, applying a creator function firstcreateAll
(array action): creates items in the state, applying a creator function firstmerge
(data action): merges an item with the existing item, applying a merging function firstmergeAll
(array action): merge many items with the existing item, applying a merging function firstmove
(move action): moves an itemreplace
(data action): replaces an item in the statereplaceAll
(array action): replaces many items in the state
Safe Reducers
These reducers provide options for handling data, e.g. they'll create data if it
doesn't exist or ignore it if it already does. They're "safe" in the sense that they
won't result in inconsistent allIds
and byId
.
addIfNew
(data action): adds an item iff it doesn't already existaddAllIfNew
(data action): adds items iff they don't already existaddOrMerge
(data action): adds or merges an existing item into the stateaddOrMergeAll
(array action): adds or merges several items into the stateaddOrReplace
(data action): adds or replaces an item in the stateaddOrReplaceAll
(array action): adds or replaces several items in the statecreateIfNew
(data action): creates an item in the state iff it doesn't existcreateAllIfNew
(array action): creates items in the state iff they don't existmoveSafe
(move action): only moves an item if the source exists, and the destination doesn'tremove
(remove action): removes an id iff it existsremoveAll
(remove many action): removes ids iff they existreplaceExisting
(data action): replaces an item iff it already existsreplaceAllExisting
(array action): replaces items iff they already existreset
(data action): resets the state to an empty normalized state, or to a state provided in action.data
Throwing Reducers
These reducers handle dangerous actions by throwing a ReducerError
. It's probably
not a good idea to use these in production code, but rather as a means of detecting
potential pitfalls during development. The error will contain a message explaining
which items in the actions caused the problem.
addOrThrow
: adds, or throws if the item already existsaddAllOrThrow
: adds many, or throws if any item exists in the statecreateOrThrow
: creates an item, or throws if the item already existscreateAllOrThrow
: creates many items, or throws if any item exists in the statemergeOrThrow
: merges an item, or throws if it doesn't exist in the statemergeAllOrThrow
: merges items, or throws if any don't exist in the statemoveOrThrow
: moves an item, or throws it doesn't exist, or there's something at the destinationreplaceOrThrow
: replaces an item, or throws if it doesn't exist in the statereplaceAllOrThrow
: replaces items, or throws if any don't exist in the state
Reducer Factories
These factories produce customized reducers, for create or merge operations. This allows you to e.g. customize default values when creating items, or change which properties are merged during a merge action. They accept, as appropriate, a creator function, or a merge function:
// These are the default creater and merger implementations, and
// they're used if you call the factories with no arguments.
const creator = data => data
const merger = (existing, merged) => ({ ...existing, ...merged })
// Create your reducers
const create = createReducer(creator)
const merge = mergeReducer(merger)
const mergeAll = mergeAllReducer(merger)
// Reset the state with a single item, plus some extra state, rather
// than empty
const reset = resetReducer({
byId: { $default: {} }, allIds: [ '$default' ], foo: 'bar'
})
// ...further customizations as needed
addOrMergeAllReducer
addOrMergeReducer
createAllIfNewReducer
createAllReducer
createIfNewReducer
createReducer
mergeAllReducer
mergeReducer
resetReducer
Utility Functions
Several utility functions are provided for working with a normalized state:
filterKnownData(state, data)
: returns items which already exist in the statefilterUnknownData(state, data)
: returns items which don't already exist in the statetoArray(state)
: Converts normalized state to any array of itemsmap(state, fn)
: Maps a function onto the result oftoArray
sort(state, fn)
: Sort the result oftoArray
usingfn
forEach(state, fn)
: CallstoArray(state).forEach(fn)
filter(state, fn)
: Filters the result oftoArray
usingfn
exists(state, id)
: Checks whetherid
is found instate
allExist(state, ids)
: Checks whether allids
are found instate
anyExist(state, ids)
: Checks whether anyids
are found instate