Security News
GitHub Removes Malicious Pull Requests Targeting Open Source Repositories
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
normalized-reducer
Advanced tools
A zero-boilerplate higher-order reducer for managing normalized relational data
A zero-boilerplate higher-order reducer for managing normalized relational data
🐒 easy to get started and use without writing any action/reducer logic
✨ handles basic CRUD, plus complex updates like entity associations and cascading changes from deletes
📦 dependency-free and framework-agnostic; use with or without Redux
🔌 integrates with Normalizr and Redux-Toolkit
Table of Contents:
Managing normalized relational data presents various complexities such as:
Normalized Reducer helps you manage normalized relational state without requiring any reducer/action boilerplate. Simply provide a declarative relational schema, and it gives you the reducers, actions, and selectors to read and write state according to that schema.
yarn add normalized-reducer
Define a schema that describes your data's relationships.
const mySchema = {
list: {
'itemIds': { type: 'item', cardinality: 'many', reciprocal: 'listId' }
},
item: {
'listId': { type: 'list', cardinality: 'one', reciprocal: 'itemIds' },
'tagIds': { type: 'tag', cardinality: 'many', reciprocal: 'itemIds'}
},
tag: {
'itemIds': { type: 'item', cardinality: 'many', reciprocal: 'tagIds' }
}
}
More info at: Top-level API > Parameter: schema
Pass in the schema, and get back a reducer, action-creators, action-types, selectors, and empty state.
import makeNormalizedSlice from 'normalized-reducer'
const {
reducer,
actionCreators,
actionTypes,
selectors,
emptyState,
} = makeNormalizedSlice(mySchema)
More info at: Top-level API > Return Value
Use the reducer
and actions
to update the state. The following example assumes the use of dispatch
from either React or React-Redux.
With React:
const [state, dispatch] = useReducer(reducer, emptyState);
With React-Redux:
const dispatch = useDispatch();
Usage:
// add entities
dispatch(actionCreators.create('item', 'i1')) // add an 'item' entity with an id of 'i1'
dispatch(actionCreators.create('list', 'l1', { title: 'first list' }), 3) // add a 'list' with id 'l1', with data, at index 3
// delete entities
dispatch(actionCreators.delete('list', 'l1')) // delete a 'list' entity whose id is 'l1'
// update entities
dispatch(actionCreators.update('item', 'i1', { value: 'do a barrel roll!' })) // update 'item' whose id is 'l1', patch (partial update)
dispatch(actionCreators.update('item', 'i1', { value: 'the sky is falling!' }, { method: 'put' })) // update, put (replacement update)
// change an entity's ordinal value
dispatch(actionCreators.move('item', 0, 1)) // move the 'item' entity at index 0 to index 1
// attach entities
dispatch(actionCreators.attach('list', 'l1', 'item', 'i1')) // attach list l1 to item i1
// detach entities
dispatch(actionCreators.detach('list', 'l1', 'item', 'i1')) // detach list l1 from item i1
// change an entity's ordinal value with respect to another entity
dispatch(actionCreators.moveAttached('list', 'l1', 'itemIds', 1 , 3)) // in item l1's .itemIds, move the itemId at index 1 to index 3
// batch: all changes will occur in a single action
dispatch(actionCreators.batch(
actionCreators.create('list', 'l10'),
actionCreators.create('item', 'i20'),
actionCreators.attach('item', 'i20', 'listId', 'l10'),
))
// sort entities
dispatch(actionCreators.sort('item', (a, b) => (a.title > b.title ? 1 : -1))) // sort items by title
// sort entities with respect to an attached entity
dispatch(actionCreators.sortAttached('list', 'l1', 'itemIds', (a, b) => (a.value > b.value ? 1 : -1))) // in item l1's .itemIds, sort by value
More info at: Action-creators API
Use the selectors
to read state.
const itemIds = selectors.getIds(state, { type: 'item' }) // ['i1', 'i2']
const items = selectors.getEntities(state, { type: 'item' }) // { 'i1': { ... }, 'i2': { ... } }
const item = selectors.getEntity(state, { type: 'item', id: 'i2' }) // { value: 'the sky is falling!', listId: 'l1' }
More info at: Selectors API
The empty state shape looks like:
{
"entities": {
"list": {},
"item": {},
"tag": {}
},
"ids": {
"list": [],
"item": [],
"tag": []
}
}
And a populated state could look like:
{
"entities": {
"list": {
"l1": { "itemIds": ["i1", "i2"] }
},
"item": {
"i1": { "listId": "l1" },
"i2": { "listId": "l1", "tagIds": ["t1"] }
},
"tag": {
"t1": { "itemIds": ["i2"] }
}
},
"ids": {
"list": ["l1"],
"item": ["i1", "i2"],
"tag": ["t1"]
}
}
Demos:
Example usage:
Normalized Reducer is comparable to Redux ORM and Redux Toolkit's entity adapter.
Comparison to Redux ORM:
Comparison to Redux Tookit's entity adapter
The top-level default export is a higher-order function that accepts a schema
and an optional namespaced
argument and returns a reducer, action-creators, action-types, selectors, and empty state.
makeNormalizedSlice<S>(schema: ModelSchema, namespaced?: Namespaced): {
reducer: Reducer<S>,
actionCreators: ActionCreators<S>,
actionTypes: ActionTypes,
selectors: Selectors<S>,
emptyState: S,
}
Example:
import makeNormalizedSlice from 'normalized-reducer';
const {
reducer,
actionCreators,
actionTypes,
selectors,
emptyState,
} = makeNormalizedSlice(mySchema, namespaced);
schema
The schema is an object literal that defines each entity and its relationships.
interface Schema {
[entityType: string]: {
[relationKey: string]: {
type: string;
reciprocal: string;
cardinality: 'one'|'many';
}
}
}
Example:
const schema = {
list: {
// Each list has many items, specified by the .itemIds attribute
// On each item, the attribute which points back to its list is .listId
itemIds: {
type: 'item', // points to schema.item
reciprocal: 'listId', // points to schema.item.listId
cardinality: 'many'
}
},
item: {
// Each item has one list, specified by the attribute .listId
// On each list, the attribute which points back to the attached items is .itemIds
listId: {
type: 'list', // points to schema.list
reciprocal: 'itemIds', // points to schema.list.itemIds
cardinality: 'one'
},
},
};
Note that type
must be an entity type (a top-level key) within the schema, and reciprocal
must be a relation key within that entity's definition.
namespaced
This is an optional argument that lets you namespace the action-types, which is useful if you are going to compose the Normalized Reducer slice with other reducer slices in your application.
Example:
const namespaced = actionType => `my-custom-namespace/${actionType}`;
If the namespaced
argument is not passed in, it defaults to normalized/
.
<S extends State>
The shape of the state, which must overlap with the following interface:
export type State = {
entities: {
[type: string]: {
[id in string|number]: { [k: string]: any }
}
},
ids: {
[type: string]: (string|number)[]
},
};
Example:
interface List {
itemIds: string[]
}
interface Item {
listId: string
}
interface State {
entities: {
list: Record<string, List>,
item: Record<string, Item>
},
ids: {
list: string[],
item: string[]
}
}
const normalizedSlice = makeNormalizedSlice<State>(schema)
Calling the top-level function will return an object literal containing the things to help you manage state:
reducer
actionCreators
actionTypes
selectors
emptyState
reducer
A function that accepts a state + action, and then returns the next state.
reducer(state: S, action: { type: string }): S
In a React setup, pass the reducer into useReducer
:
function MyComponent() {
const [normalizedState, dispatch] = useReducer(reducer, emptyState)
}
In a Redux setup, compose the reducer with other reducers, or use it as the root reducer:
const { reducer } = makeNormalizedSlice(schema)
// compose it with combineReducers
const reduxReducer = combineReducers({
normalizedData: reducer,
//...
})
// or used it as the root reducer
const store = createStore(reducer)
actionCreators
An object literal containing action-creators. See the Action-creators API section.
actionTypes
An object literal containing the action-types.
const {
CREATE,
DELETE,
UPDATE,
MOVE,
ATTACH,
DETACH,
MOVE_ATTACHED,
SORT,
SORT_ATTACHED,
BATCH,
SET_STATE,
} = actionTypes
Their values are namespaced according to the namespaced
parameter of the top-level function. Example: normalized/CREATE
selectors
An object literal containing the selectors. See the Selectors API section.
emptyState
An object containing empty collections of each entity.
Example:
{
"entities": {
"list": {},
"item": {},
"tag": {}
},
"ids": {
"list": [],
"item": [],
"tag": []
}
}
An action-creator is a function that takes parameters and returns an object literal describing how the reducer should enact change upon state.
create
Creates a new entity
( entityType: string,
id: string|number,
data?: object,
index?: number
): CreateAction
Parameters:
entityType
: the entity typeid
: an id that doesn't belong to an existing entitydata
: optional, an object of arbitrary, non-relational dataindex
: optional, a number greater than 0Note:
id
should be a string or number provided by your code, such as a generated uuidid
already belongs to an existing entity, then the action will be ignored.data
is provided, then the entity will be initialized as an empty object.data
, then they will be ignored; to add relational data, use the attach
action-creator after creating the entity.index
is provided, then the entity will be inserted at that position in the collection, and if no index
is provided the entity will be appended at the end of the collection.Example:
// create a list with a random uuid as the id, and a title, inserted at index 3
const creationAction = actionCreators.create('list', uuid(), { title: 'shopping list' }, 3)
Demos:
delete
Deletes an existing entity
( entityType: string,
id: string|number,
cascade?: SelectorTreeSchema
): DeleteAction
Parameters:
entityType
: the entity typeid
: the id of an existing entitycascade
: optional, an object literal describing a cascading deletionNote:
cascade
to delete entities that are attached to the deletable entityBasic Example:
// deletes a list whose id is 'l1', and automatically detaches any entities currently attached to it
const deletionAction = actionCreators.delete('list', 'l1');
Cascade Example:
/*
deletes list whose id is 'l1',
deletes any items attached to 'l1'
deletes any tags attached to those items
detaches any entities attached to the deleted entities
*/
const deletion = actionCreators.delete('list', 'l1', { itemIds: { tagIds: {} } });
Demos:
update
Updates an existing entity
( entityType: string,
id: string|number,
data: object,
options?: { method?: 'patch'|'put' }
): UpdateAction
Parameters:
entityType
: the entity typeid
: the id of an existing entitydata
: an object of any arbitrary, non-relational dataoptions.method
: optional, whether to partially update or completely replace the entity's non-relational dataNote:
id
does not exist, then the action will be ignoreddata
, then they will be ignored; to update relational data, use the attach
and detach
action-creators.method
option is provided, then it will default to a patch (partial update)Example:
// updates a list whose id is 'l1', partial-update
const updateAction = actionCreators.update('list', 'l1', { title: 'do now!' })
// updates a list whose id is 'l1', full replacement
const updateAction = actionCreators.update('list', 'l1', { title: 'do later' }, { method: 'put' })
Demos:
attach
Attaches two existing related entities
( entityType: string,
id: string|number,
relation: string,
relatedId: string|number,
options?: { index?: number; reciprocalIndex?: number }
): AttachAction
Parameters:
entityType
: the entity typeid
: the id of an existing entityrelation
: a relation key or relation typeattachableId
: the id of an existing entity to be attachedoptions.index
: optional, the insertion index within the entity's attached-id's collectionoptions.reciprocalIndex
: optional, same as options.index
, but the opposite directionNote:
Example:
/*
attaches item 'i1' to tag 't1'
in item i1's tagIds array, t1 will be inserted at index 2
in tag t1's itemIds array, i1 will be inserted at index 3
*/
const attachmentAction = actionCreators.attach('item', 'i1', 'tagIds', 't1', 2, 3);
Displacement example:
// attach list 'l1' to item 'i1'
const firstAttachment = actionCreators.attach('list', 'l1', 'itemId', 'i1');
// attach list 'l20' to item 'i1'
// this will automatically detach item 'i1' from list 'l1'
const secondAttachment = actionCreators.attach('list', 'l20', 'itemId', 'i1');
Demos:
detach
Detaches two attached entities
( entityType: string,
id: string|number,
relation: string,
detachableId: string|number
): DetachAction
Parameters:
entityType
: the entity typeid
: the id of an existing entityrelation
: a relation key or relation typedetachableId
: the id on an existing entity to be attachedExample:
// detach item 'i1' from tag 't1'
const detachmentAction = actionCreators.detach('item', 'i1', 'tagIds', 't1')
Demos:
move
Changes an entity's ordinal position
( entityType: string,
src: number,
dest: number
): MoveAction
Parameters:
entityType
: the entity typesrc
: the source/starting index of the entity to repositiondest
: the destination/ending index; where to move the entity toNote:
src
or dest
is less than 0, then the action will be ignoredsrc
greater than the highest index, then the last entity will be moveddest
greater than the highest index, then, the entity will be move to last positionExample:
// move the item at index 2 to index 5
const moveAction = actionCreators.move('item', 2, 5)
Demos:
moveAttached
Changes an entity's ordinal position with respect to an attached entity
( entityType: string,
id: string|number,
relation: string,
src: number,
dest: number
): MoveAttachedAction
Parameters:
entityType
: the entity typeid
: the id of an existing entityrelation
: the relation key of the collection containing the id to movesrc
: the source/starting index of the entity to repositiondest
: the destination/ending index; where to move the entity toNote:
id
does not exist, then the action will be ignoredsrc
or dest
is less than 0, then the action will be ignoredsrc
greater than the highest index, then the last entity will be moveddest
greater than the highest index, then the entity will be move to last positionExample:
// in list l1's itemIds array, move itemId at index 2 to index 5
const moveAction = actionCreators.moveAttached('list', 'l1', 'itemIds', 2, 5)
Demos:
sort
Sorts a top-level entity ids collection
<T>(
entityType: string,
compare: (a: T, b: T) => number
): SortAction
Parameters:
entityType
: the entity typecompare
: the sorting comparison functionExample:
// sort list ids (state.ids.list) by title
const sortAction = actionCreators.sort('list', (a, b) => (a.title > b.title ? 1 : -1))
Demos:
sortAttached
Sorts an entity's attached-ids collection
<T>(
entityType: string,
id: string|number,
relation: string,
compare: Compare<T>
): SortAction
Parameters:
entityType
: the entity typeid
: the id of an existing entityrelation
: the relation key or relation type of the collection to sortcompare
: the sorting comparison functionNote:
id
does not exist, then the action will be ignoredExample:
// in list l1, sort the itemsIds array by by value
const sortAction = actionCreators.sort('list', 'l1', 'itemIds', (a, b) => (a.value > b.value ? 1 : -1))
Demos:
batch
Runs a batch of actions in a single reduction
(...actions: Action[]): BatchAction
Parameters:
...actions
: Normalized Reducer actions excluding batch
and setState
Note:
Example:
// create list 'l1', then create item 'i1', then attach them to each other
const batchAction = actionCreators.batch(
actionCreators.create('list', 'l1'),
actionCreators.create('item', 'i1'),
actionCreators.attach('list', 'l1', 'itemIds', 'i1'), // 'l1' and 'i1' would exist during this action due to the previous actions
// nested batch-actions are also accepted
actionCreators.batch(
actionCreators.create('item', 'i2'),
actionCreators.create('item', 'i3'),
)
)
Demos:
setState
Sets the normalized state
(state: S): SetStateAction
Parameters:
state
: the state to setNote:
Example:
const state = {
entities: {
list: {
l1: { title: 'first list', itemIds: ['i1'] },
l2: {}
},
item: {
i1: { value: 'do a barrel roll', listId: 'l1', tagIds: ['t1'] }
},
tag: {
t1: { itemIds: ['i1'], value: 'urgent' }
}
},
ids: {
list: ['l1', 'l2'],
item: ['i1'],
tag: ['t1']
}
}
const setStateAction = actionCreators.setState(state)
Demos:
Each selector is a function that takes the normalized state and returns a piece of the state. Currently, the selectors API is minimal, but are enough to access any part of the state slice so that you can build your own application-specific selectors.
getIds
Returns an array of ids of a given entity type
(state: S, args: { type: string }): (string|number)[]
Parameters:
state
: the normalized stateargs.type
: the entity typeExample:
const listIds = selectors.getIds(state, { type: 'item' }) // ['l1', 'l2']
getEntities
Returns an object literal mapping each entity's id to its data
<E>(state: S, args: { type: string }): Record<(string|number), E>
Parameters:
state
: the normalized stateargs.type
: the entity typeGeneric Parameters:
<E>
: the entity's typeExample:
const lists = selectors.getEntities(state, { type: 'item' })
/*
{
l1: { title: 'first list', itemIds: ['i1', 'i2'] },
l2: { title: 'second list', itemIds: [] }
}
*/
getEntity
Returns an entity by its type and id
<E>(state: S, args: { type: string; id: string|number }): E | undefined
Parameters:
state
: the normalized stateargs.type
: the entity typeargs.id
: the entity idGeneric Parameters:
<E>
: the entity's typeNote:
Example:
const lists = selectors.getEntity(state, { type: 'item', id: 'i1' })
/*
{ title: 'first list', itemIds: ['i1', 'i2'] }
*/
The top-level named export fromNormalizr
takes normalized data produced by a normalizr normalize
call and returns state that can be fed into the reducer.
Example:
import { normalize } from 'normalizr'
import { fromNormalizr } from 'normalized-reducer'
const denormalizedData = {...}
const normalizrSchema = {...}
const normalizedData = normalize(denormalizedData, normalizrSchema);
const initialState = fromNormalizr(normalizedData);
Demos:
MIT
FAQs
A zero-boilerplate higher-order reducer for managing normalized relational data
We found that normalized-reducer demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.