redux-normalized-module
A library for generating a redux normalized module
The genereated module provides a reducer, the action creators and the selectors to manage a normalized state.
Installation
npm install @intactile/redux-normalized-module
or
yarn add @intactile/redux-normalized-module
Normalized state
A normalized state is structured like this:
{
byId: {
1: { id: 1, name: 'France', continentId: 1 },
2: { id: 2, name: 'Croatia', continentId: 1 },
3: { id: 3, name: 'Belgium', continentId: 1 },
4: { id: 4, name: 'England', continentId: 1 },
5: { id: 5, name: 'Brasil', continentId: 2 },
},
allIds: [1, 2, 3, 4]
}
Indexes can also be added.
{
...,
byContinentId: {
1: [1, 2, 3, 4],
2: [5]
}
}
Configuration
A normalized module is created from a definition.
const moduleDefinition = {
selector: state => state.countries,
reducers: {
// map an action type with a generic reducer
CREATE_COUNTRY: "create",
UPDATE_COUNTRY: "update",
REPLACE_COUNTRY: "replace",
DELETE_COUNTRY: "delete",
LOAD_COUNTRIES: {
type: "load",
extractor: action => action.payload.countries
}
}
};
const module = createReduxNormalizedModule(moduleDefinition);
The generated module will expose a reducer:
import { createStore, combineReducers } from "redux";
store = createStore(combineReducers({ countries: module.reducer }));
some action creators:
store.dispatch(module.actions.create({ name: "Argentina", continentId: 2 }));
store.dispatch(module.actions.update(6, { name: "Chile" }));
store.dispatch(
module.actions.replace({ id: 6, name: "Chile", continentId: 2 })
);
store.dispatch(module.actions.delete(6));
store.dispatch(module.actions.toFront(2));
and some selectors:
const state = store.getState();
module.selectors.getAllIds(state);
module.selectors.getAll(state);
module.selectors.getById(state, 6);
module.selectors.isEmpty(state);
module.selectors.getLastCreated(state);
module.selectors.getLastCreatedId(state);
module.selectors.getNextId(state);
Add a comparator
The state could be sorted with a comparator:
const sortByName = (o1, o2) => o1.name.localeCompare(o2.name);
const moduleDefinition = {
comparator: sortByName
};
By doing this, the allIds
array with automatically sorted when an object is created or updated.
module.selectors.getAllIds(state); // => the ids sorted by `name`
Note: the toFront
action can't be used when a comparator is configured
Add a many to one index
For performance purpose, one or more indexes might be added to the state. They are updated when an object is created, updated or deleted.
const moduleDefinition = {
indexes: [{ attribute: "continentId" }]
};
The generated module provides an index selector:
const state = store.getState();
selectors.byContinentId.get(store.getState(), 1); // => [1, 2, 3, 4]
selectors.byContinentId.get(store.getState(), 3); // => []
The index can also be sorted with a comparator:
const sortByName = (o1, o2) => o1.name.localeCompare(o2.name);
const moduleDefinition = {
indexes: [{ attribute: "continentId", comparator: sortByName }]
};
The attribute property can also be an object to handle more complex use case it should provide a name property, this will be use to generate the name of the index and it should also provide a custom function to compute the keys of the index, this function will be called with the object as parameter:
const moduleDefinition = {
indexes: [{
attribute: {
name: "isColored",
computeKey: (object) => !!object.color
}
}]
};
Add a one to one index
const moduleDefinition = {
indexes: [{ attribute: "name", oneToOne: true }]
};
The index selectors are:
const state = store.getState();
selectors.byName.get(store.getState(), "France"); // => { id: 1, name: 'France', continentId: 1 }
selectors.byName.exists(store.getState(), "Germany"); // => false
Complete module definition
const moduleDefinition = {
indexes: [
{ attribute: "continentId", comparator: sortByName },
{ attribute: "name", oneToOne: true }
],
selector: state => state.countries,
reducers: {
CREATE_COUNTRY: "create",
UPDATE_COUNTRY: "update",
REPLACE_COUNTRY: "replace",
DELETE_COUNTRY: "delete",
LOAD_COUNTRIES: {
type: "load",
extractor: action => action.payload.countries
}
}
};