Managing normalized state in ngrx applications, transparently.
This package provides a set of actions, reducers and selectors for handling normalization and denormalization of state data transparently. ngrx-normalizr uses normalizr for normalizing and denormalizing data. All normalization and denormalization is defined by the use of normalizr schemas, since that's the way normalizr works. This enables selectors to use a transparent and powerful projection of state data.
Releases will be published from the
master
branch. Go there for documentation that aligns with the npm repo version.
To install this package:
yarn add ngrx-normalizr
npm i ngrx-normalizr
ngrx-normalizr @ngrx-store as its peer dependencies, so you need to install them if not present already:
ngrx-normalizr itself does not rely on any Angular feature.
yarn add @ngrx/store
npm i @ngrx/store
Also refer to the Typedoc documentation.
To enable the normalizing reducer to store normalized data, you have to add it to your state. The best place for this might be the root state of your application, but feature states may use their own normalized state as well. Extend your state interface with the NormalizedState
interface. TheActionReducerMap
has to implement a reducer which reduces the state to a NormalizedState
.
import { ActionReducerMap } from '@ngrx/store';
import { NormalizedState, normalized } from 'ngrx-normalizr';
export interface State extends NormalizedState {
/* ... other state properties */
}
export const reducers: ActionReducerMap<State> = {
normalized,
/* ... other state reducers */
};
If there are no other state properties, it is sufficient to add the ngrx-normalizr reducer to your state reducers or simply pass it to StoreModule.forRoot
.
export const reducers: ActionReducerMap<NormalizedState> = { normalized };
Now you have a normalized
state property which will hold the normalized data. Do not worry about the weird name,
you will not have to deal with it.
Schemas define the relations of your data. In order to normalize and denormalize data, normalizr needs to be feed with a schema. In this example, a user might have an array of pets:
import { schema } from 'normalizr';
export class Pet {
id: string;
name: string;
type: 'cat' | 'dog';
}
export class User {
id: string;
name: string;
pets: Pet[];
}
export const petSchema = new schema.Entity('pets');
export const userSchema = new schema.Entity('users', { pets: [petSchema] });
Actions are used to set data in - and remove data from - the normalized store.
To add data and automatically normalize it, ngrx-normalizr provides a AddData
action. This action takes an object with data
and schema
as an argument. Entities are identified by their id attribute set in the passed schema.
Existing entities will be overwritten by updated data, new entities will be added to the store. For adding related childs, an AddChildData
action is provided.
@Effect()
loadEffect$ = this.actions$
.ofType(LOAD)
.switchMap(action => this.http.get('https://example.com/api/user'))
.mergeMap((data: User[]) => [
// dispatch to add data to the store
new AddData<User>({ data, schema }),
// dispatch to inform feature reducer
new LoadSuccess(data)
])
.catch(err => Observable.of(new LoadFail(err)));
Adding a related child data to a parent entity can be done with the AddChildData
action. Note that for this to work, the relation has to be defined in the schema. The action takes a couple of arguments which need to be given in an object:
data
: Array of child entities to addchildSchema
Theschema.Entity
of the child entityparentSchema
: Theschema.Entity
of the parent entityparentId
: The id of the entity to add child references to
@Effect()
addPetEffect$ = this.actions$
.ofType(ADD_PET)
.switchMap(action => this.http.post('https://example.com/api/pets'))
.mergeMap((data: Pet[]) => [
// dispatch to add data to the store
new AddChildData<Pet>({ data, childSchema, parentSchema, parentId }),
// dispatch to inform feature reducer
new AddPetSuccess(data)
])
.catch(err => Observable.of(new LoadFail(err)));
The SetData
action will overwrite all entities for a given schema with the normalized entities of the data
property of the action constructor argument. This action can
be used for resetting entity state data instead of adding and updating existing entities.
To remove data, ngrx-normalizr provides a RemoveData
action.
This action takes an object with id
, schema
and an optional removeChildren
property as constructor argument. The schema entity with the given id will be removed. If removeChildren
is a map of the schema key mapped to an object property, all referenced child entities will also be removed from the store. This is handy for 1:1 relations, since only removing the parent entity may leave unused child entities in the store.
@Effect()
removeEffect$ = this.actions$
.ofType(REMOVE)
.switchMap((action: Remove) => this.http.delete(`https://example.com/api/user/${action.payload.id}`))
.mergeMap(result => [
// dispatch to remove data from the store
new RemoveData({ id: result.id, schema, removeChildren: { pets: 'pets' } }),
// dispatch to inform feature reducer
new RemoveSuccess()
])
.catch(err => Observable.of(new RemoveFail(err)));
Removing a child entity which is 1:1 related to a parent entity can be done with the RemoveChildData
action. Note that for this to work, the relation has to be defined in the schema. The action takes a couple of arguments which need to be given in an object:
id
: Id of the child entity that should be removedchildSchema
Theschema.Entity
of the child entityparentSchema
: Theschema.Entity
of the parent entityparentId
: The id of the entity to remove child references from
@Effect()
removePetEffect$ = this.actions$
.ofType(REMOVE_PET)
.switchMap(action => this.http.remove(`https://example.com/api/pets/${action.payload.id}`))
.mergeMap((data: Pet) => [
// dispatch to add data to the store
new RemoveChildData({ id: data.id, childSchema, parentSchema, parentId }),
// dispatch to inform feature reducer
new RemovePetSuccess(data)
])
.catch(err => Observable.of(new LoadFail(err)));
For convenience, ngrx-normalizr provides an actionCreators
function which will return an object with following schema bound action creators:
setData
-(data: T[]) => SetData<T>
addData
-(data: T[]) => AddData<T>
addChildData<C>
-(data: C[], childSchema: schema.Entity, parentId: string) => AddChildData
removeData
-(id: string, removeChildren?: SchemaMap) => RemoveData
removeChildData
-(id: string, childSchema: schema.Entity, parentId: string) => RemoveChildData
Action creators could be exported along whith other feature actions:
import { actionCreators } from 'ngrx-normalizr';
const creators = actionCreators<User>(userSchema);
export const setUserData = creators.setData;
export const addUserData = creators.addData;
export const removeUserData = creators.removeData;
Using the action creator in an Effect class:
import { removeUserData } from '../actions';
@Effect()
removeEffect$ = this.actions$
.ofType(REMOVE)
.switchMap((action: Remove) => this.http.delete(`https://example.com/api/user/${action.payload.id}`))
.mergeMap(result => [
// dispatch to remove data from the store
removeUserData(id: result.id, { pets: 'pets' }),
// dispatch to inform feature reducer
new RemoveSuccess()
])
.catch(err => Observable.of(new RemoveFail(err)));
ngrx-normalizr provides two simple selectors and two simple projector functions to query the state and project/denormalize the result.
To transparently query data from the store from a feature module, selectors are provided by the createSchemaSelectors
function.
It takes an entity schema to create schema bound selectors:
import { createSchemaSelectors } from 'ngrx-normalizr';
import { User } from '../classes/user';
const schemaSelectors = createSchemaSelectors<User>(userSchema);
createSchemaSelectors
will return schema bound selectors (instance of SchemaSelectors
):
getEntities
-MemoizedSelector<{}, T[]>
Returns all denormalized entities for the schemagetNormalizedEntities
-MemoizedSelector<any, EntityMap>
Returns all normalized (raw) state entities of every schema (the whole entities state)entitiesProjector
-(entities: {}, ids?: Array<string>) => T[]
Projector function for denormalizing a the set of normalized entities to an denormalized entity array. If noids
are given, all entities will be denormalized.entityProjector
-(entities: {}, id: string) => T
Projector function for denormalizing a single normalized entity with the given id
You might create several selectors with several schemas, i.e. a listView schema, which only denormalizes the data used in the list view, and a detailView schema, to completely denormalize a given entity.
Feature selectors can use the schema bound selectors and projector functions to query entity data from the store. To get all denormalized
entities, you might simply use the getEntities
selector like this:
// store.select(getUsers) will give all denormalized user entities
const getUsers = schemaSelectors.getEntities;
Under the hood this does something similar to this:
// equivalent alternative
const getUsers = createSelector(
schemaSelectors.getNormalizedEntities,
schemaSelectors.entitiesProjector
);
The entitiesProjector
simply takes an object of normalized entity data and applies the denormalization with the bound schema. Optionally an array of id strings can be passed as a second parameter to perform denormalization for the given id's only.
To query and denormalize specific data you can use the @ngrx/store createSelectors
function and compose them with the schema bound
selectors:
import { createSelector } from '@ngrx/store';
export const getSelectedUserId = createSelector(
userFeatureSelector,
user.getSelectedId
);
// store.select(getSelectedUser) will give the denormalized selected user
const getSelectedUser = createSelector(
schemaSelectors.getNormalizedEntities,
getSelectedUserId,
schemaSelectors.entityProjector
);
entityProjector
will simply take an object of denormalized entities and apply the denormalization with the bound schema only for the given id. Note that you might also select data from the denormalized result and providing your own selector:
const getSelectedUserWithPetsOnly = createSelector(
getUsers,
getSelectedId,
(entities, id) => entities.find(e => e.id === id && e.pets.length > 0)
);
Michael Krone – @DevDig – [email protected] and all CONTRIBUTORS
Distributed under the MIT license. See LICENSE
for more information.
https://github.com/michaelkrone/ngrx-normalizr
- Fork it (https://github.com/michaelkrone/ngrx-normalizr)
- Create your feature branch (
git checkout -b feature/fooBar
) - Commit your changes (
git commit -am 'Add some fooBar'
) - Push to the branch (
git push origin feature/fooBar
) - Create a new Pull Request