7

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.

Street0
  • 234
  • 3
  • 15

3 Answers3

5

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

Street0
  • 234
  • 3
  • 15
  • Although this requires hell of a lot boilerplate code – Street0 Sep 14 '19 at 19:23
  • Just have a query, in case for a particular training, if just one exercise need to be removed for a particular training where that data is stored? We cannot juts remove the exercise form excerciseState. So the array data structure that hold information for an exercise how that is fetched in form ? – Mr X Jul 27 '20 at 18:45
  • Or the normaliser maintains the ids of the exercise in the parentState? – Mr X Jul 27 '20 at 18:49
1

@ngrx/entity does not provide ways to normalize data. You can use normalizr in combination with @ngrx/entity, you could for example normalize your data in the Angular service/NgRx Effect/NgRx reducer.

The Redux docs has some pages about using normalizer with redux at https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape.

timdeschryver
  • 14,415
  • 1
  • 19
  • 32
  • Oh okay. I believed it does because the first root level of data seems to be normalized but nested doesn't. How would I go about mixing normalizr output with ngrx/entity ? It doesn't seem practical. Creating 3 different EntityStates for each entity... also adapter which makes code way cleaner and easier doesn't work with data other than T (entity). Normalizr outputs dictionary and that can be assigned directly to the ngrx/entity EntityState entities property. But you are missing ids portion for each EntityState except the root one. Which normalizr does provide for. – Street0 Sep 13 '19 at 08:39
  • Do you happen to have some example of that combination normalizr + @ngrx/entity @timdeschryver? – Street0 Sep 13 '19 at 08:39
  • No sorry, I haven't encountered one – timdeschryver Sep 13 '19 at 12:19
0

I think there are 2 solutions

  1. use https://github.com/paularmstrong/normalizr to normalizr your data

  2. use https://ngrx.io/guide/entity/adapter#adapter-collection-methods map function to do deep clone.

return adapter.map(
  entity => entity.id == obj.id ? {...entity, foo: 'bar'} : entity, 
  state
);

or you can use https://immerjs.github.io/immer/docs/introduction to handle this

return adapter.map(
  entity => entity.id == obj.id ? produce(entity, (draft: any) => { draft.foo = 'bar' }) : entity, 
  state
);

maxisam
  • 21,975
  • 9
  • 75
  • 84