Search code examples
normalizationngrxangular8state-managementngrx-entity

How to normalize deeply nested data with ngrx/entity (EntityState and EntityAdapter)


I wish to normalize data from my server so I can use it more easily with ngrx/entity.

I like how ngrx/entity reduces complexity of reducers and other stuff by providing EntityState interface and EntityAdapter. However I don't find it to work good with nested data.

I have 3 levels of data:

Training -> exercises -> sets

If I use this with classic pattern of ngrx/entity it gets crowded fast when I work with nested data.

Below is first thing I ran onto when using ngrx/entity This is how data gets normalized when I put all trainings data in After that I snooped around and got to normalizr library Output I like how normalizr normalizes my data and also replaces nested array values with only id as keys to other entities (exercises, sets)

What I tried first was combine multiple entity states like so: Entity states But this requires changing up my server and a lot of logic and effort.

What I'd like is to somehow combine normalizr with ngrx/entity.. Get the same thing normalizr gives me but have the freedom to use entity adapter api from ngrx/entity it's selectors and other code that's at my service from ngrx/entity

So bottom line my question would be how to normalize deep nested data with ngrx/entity (like normalizr library does) without some kind of server effort.


Solution

  • So I found some workaround solution while still using NGRX

    Before I start I just want to say that ngrx also has ngrx/data pack which provides less boilerplate. But while I was reading about it I found a definitive answer to my question:

    https://ngrx.io/guide/data/limitations "This library shallow-clones the entity data in the collections. It doesn't clone complex, nested, or array properties. You'll have to do the deep equality tests and cloning yourself before asking NgRx Data to save data."

    I believe this is also true for ngrx/entity.

    I started to look for alternative solutions: BreezeJs, NGXS, Akita from which I only found NGXS understandable to me fast but would require effort to detach my ngrx implementation from project.

    So I got back to ngrx and tried to do a workaround for 3 levels deep nested data

    Create 3 separate entity states ( I'll try to use ngrx/data that could certanly reduce all the boilerplate)

    Create a function that will return all necessary entities and ids for each entity (use normalizr for normalization)

    export function normalizeTrainingArray(trainings: Training[]) {
    var normalized = normalize(trainings, trainingsSchema);
    
    var entities = {
        trainings: {},
        exercises: {},
        sets: {}
    }
    entities.trainings = normalized.entities.trainings ? normalized.entities.trainings : {};
    entities.exercises = normalized.entities.exercises ? normalized.entities.exercises : {};
    entities.sets = normalized.entities.sets ? normalized.entities.sets : {};
    
    var ids = {
        trainingIds: [],
        exerciseIds: [],
        setIds: []
    }
    ids.trainingIds = normalized.entities.trainings ? Object.values(normalized.entities.trainings).map(x => x.id) : [];
    ids.exerciseIds = normalized.entities.exercises ? Object.values(normalized.entities.exercises).map(x => x.id) : [];
    ids.setIds = normalized.entities.sets ? Object.values(normalized.entities.sets).map(x => x.id) : [];
    
    return {
        entities,
        ids
    }
    

    Something like this will suffice. Send normalizeData action and use effect to call this method and dispatch 3 different actions for fetchedData...

    Something along the lines of:

       trainingsNormalized$ = createEffect(() =>
        this.actions$.pipe(
            ofType(TrainingActions.normalizeTrainings),
            tap(payload => {
    
                var normalized = normalizeTrainingArray(payload.trainings);
    
                this.store.dispatch(TrainingActions.trainingsFetched({ entities: normalized.entities.trainings, ids: normalized.ids.trainingIds }))
                this.store.dispatch(ExerciseActions.exercisesFetched({ entities: normalized.entities.exercises, ids: normalized.ids.exerciseIds }))
                this.store.dispatch(SetActions.setsFetched({ entities: normalized.entities.sets, ids: normalized.ids.setIds }))
            })
        )
        , { dispatch: false });
    

    And in one sample reducer:

        // GET ALL
    on(TrainingActions.trainingsFetched, (state: TrainingState, payload: { entities: Dictionary<Training>, ids: string[] }) => {
        return {
            ...state,
            entities: payload.entities,
            ids: payload.ids
        }
    }),
    

    Result is:

    result