How do you add/remove to a redux store generated with normalizr?
Asked Answered
T

5

43

Looking the examples from the README:

Given the "bad" structure:

[{
  id: 1,
  title: 'Some Article',
  author: {
    id: 1,
    name: 'Dan'
  }
}, {
  id: 2,
  title: 'Other Article',
  author: {
    id: 1,
    name: 'Dan'
  }
}]

It's extremely easy to add a new object. All I have to do is something like

return {
  ...state,
  myNewObject
}

In the reducer.

Now given the structure of the "good" tree, I have no idea how I should approach it.

{
  result: [1, 2],
  entities: {
    articles: {
      1: {
        id: 1,
        title: 'Some Article',
        author: 1
      },
      2: {
        id: 2,
        title: 'Other Article',
        author: 1
      }
    },
    users: {
      1: {
        id: 1,
        name: 'Dan'
      }
    }
  }
}

Every approach I've thought of requires some complex object manipulation, which makes me feel like I'm not on the right track because normalizr is supposed to be making my life easier.

I can't find any examples online of someone working with the normalizr tree in this way. The official example does no adding and removing so it was no help either.

Could someone let me know how to add/remove from a normalizr tree the right way?

Throb answered 22/1, 2016 at 19:47 Comment(0)
T
35

The following is directly from a post by the redux/normalizr creator here:

So your state would look like:

{
  entities: {
    plans: {
      1: {title: 'A', exercises: [1, 2, 3]},
      2: {title: 'B', exercises: [5, 1, 2]}
     },
    exercises: {
      1: {title: 'exe1'},
      2: {title: 'exe2'},
      3: {title: 'exe3'}
    }
  },
  currentPlans: [1, 2]
}

Your reducers might look like

import merge from 'lodash/object/merge';

const exercises = (state = {}, action) => {
  switch (action.type) {
  case 'CREATE_EXERCISE':
    return {
      ...state,
      [action.id]: {
        ...action.exercise
      }
    };
  case 'UPDATE_EXERCISE':
    return {
      ...state,
      [action.id]: {
        ...state[action.id],
        ...action.exercise
      }
    };
  default:
    if (action.entities && action.entities.exercises) {
      return merge({}, state, action.entities.exercises);
    }
    return state;
  }
}

const plans = (state = {}, action) => {
  switch (action.type) {
  case 'CREATE_PLAN':
    return {
      ...state,
      [action.id]: {
        ...action.plan
      }
    };
  case 'UPDATE_PLAN':
    return {
      ...state,
      [action.id]: {
        ...state[action.id],
        ...action.plan
      }
    };
  default:
    if (action.entities && action.entities.plans) {
      return merge({}, state, action.entities.plans);
    }
    return state;
  }
}

const entities = combineReducers({
  plans,
  exercises
});

const currentPlans = (state = [], action) {
  switch (action.type) {
  case 'CREATE_PLAN':
    return [...state, action.id];
  default:
    return state;
  }
}

const reducer = combineReducers({
  entities,
  currentPlans
});

So what's going on here? First, note that the state is normalized. We never have entities inside other entities. Instead, they refer to each other by IDs. So whenever some object changes, there is just a single place where it needs to be updated.

Second, notice how we react to CREATE_PLAN by both adding an appropriate entity in the plans reducer and by adding its ID to the currentPlans reducer. This is important. In more complex apps, you may have relationships, e.g. plans reducer can handle ADD_EXERCISE_TO_PLAN in the same way by appending a new ID to the array inside the plan. But if the exercise itself is updated, there is no need for plans reducer to know that, as ID has not changed.

Third, notice that the entities reducers (plans and exercises) have special clauses watching out for action.entities. This is in case we have a server response with “known truth” that we want to update all our entities to reflect. To prepare your data in this way before dispatching an action, you can use normalizr. You can see it used in the “real world” example in Redux repo.

Finally, notice how entities reducers are similar. You might want to write a function to generate those. It's out of scope of my answer—sometimes you want more flexibility, and sometimes you want less boilerplate. You can check out pagination code in “real world” example reducers for an example of generating similar reducers.

Oh, and I used { ...a, ...b } syntax. It's enabled in Babel stage 2 as ES7 proposal. It's called “object spread operator” and equivalent to writing Object.assign({}, a, b).

As for libraries, you can use Lodash (be careful not to mutate though, e.g. merge({}, a, b} is correct but merge(a, b) is not), updeep, react-addons-update or something else. However if you find yourself needing to do deep updates, it probably means your state tree is not flat enough, and that you don't utilize functional composition enough. Even your first example:

case 'UPDATE_PLAN':
  return {
    ...state,
    plans: [
      ...state.plans.slice(0, action.idx),
      Object.assign({}, state.plans[action.idx], action.plan),
      ...state.plans.slice(action.idx + 1)
    ]
  };

can be written as

const plan = (state = {}, action) => {
  switch (action.type) {
  case 'UPDATE_PLAN':
    return Object.assign({}, state, action.plan);
  default:
    return state;
  }
}

const plans = (state = [], action) => {
  if (typeof action.idx === 'undefined') {
    return state;
  }
  return [
    ...state.slice(0, action.idx),
    plan(state[action.idx], action),
    ...state.slice(action.idx + 1)
  ];
};

// somewhere
case 'UPDATE_PLAN':
  return {
    ...state,
    plans: plans(state.plans, action)
  };
Throb answered 22/1, 2016 at 21:1 Comment(10)
thanks @AR7 for this brilliant explanation. I have one question: why do we need to keep the currentPlans array in the state and to keep it updated (well, if you have it the state, of course, the least is to update it, but what is it used for elsewhere)? Isn't it enough to have the plans' object in the state? What is it used for in practice? I've noticed that the Redux documentation as well as the normalizr documentation mention these arrays.Cairngorm
@Cedric From my point of view it's used to keep the order of the objects. HashMaps have no order so if you kept only the plans object, every time you refresh the page the order could be completely different. Also you can't iterate over objects in any MVC framework so you'd need to do something like Object.keys(plans).map() in react instead of just using the current plans array.Throb
@Cedric also I didn't write this haha. It was the creator of redux/normalizr himself!Throb
thanks @AR7. To say the truth, I also keep it because I find it easier to control iteration over a list of items in React indexes components through arrays than through for...in loops. But I was not so sure why it was systematically kept in the examples provided by the doc. You might want to take a look at the question I've set here: #34965132 :-) I'm sure you'd have helpful advices! You'll probably tell me I need to further normalize...Cairngorm
another interesting post on normalizr's github explain what the authors of normalizr do. They store as objects and convert objects to array when rendering in the React views: github.com/gaearon/normalizr/issues/15 So the mystery about why these arrays are kept in the store remains...Cairngorm
The reason why gaearon (Dan Abramov, the creator of Redux) keeps the arrays in the store, in parallel to flat maps, is explained in this github issue: github.com/rackt/redux/issues/316Cairngorm
Nice explanation! So how would you delete? {...state, [action.id]: undefined} ?Clayborn
@NikSo that's exactly why I'm here.....no where do I see any mention of the idomatic way to remove entities from a normalized store? I find it hard to believe that we are the only ones....did u get to the bottom of it?Suffragist
Maybe this tutorial could someone help robinwieruch.de/the-soundcloud-client-in-react-redux-normalizrSiderolite
@NikSo you could do it in multiple steps. Something like const newState = {...state}, and then delete newState[action.id] and then return newState. Mutation is fine if you aren't mutating the old state.Throb
S
4

Most of the time I use normalizr for data which I get from an API, because I don't have any control over the (usually) deep nested data structures. Let's differentiate Entities and Result and their usage.

Entities

All the pure data is in the entities object after it has been normalized (in your case articles and users). I would recommend either to use a reducer for all entities or a reducer for each entity type. The entity reducer(s) should be responsible to keep your (server) data in sync and to have a single source of truth.

const initialState = {
  articleEntities: {},
  userEntities: {},
};

Result

The results are only references to your entities. Imagine the following scenario: (1) You fetch from an API recommended articles with ids: ['1', '2']. You save the entities in your article entity reducer. (2) Now you fetch all articles written by a specific author with id: 'X'. Again you sync the articles in the article entity reducer. The article entity reducer is the single source of truth for all your article data - thats it. Now you want to have another place to differentiate the articles ((1) recommended articles and (2) articles by author X). You can easily keep these in another use case specific reducer. The state of that reducer might look like this:

const state = {
  recommended: ['1', '2' ],
  articlesByAuthor: {
    X: ['2'],
  },
};

Now you can easily see that the article by author X is a recommended article as well. But you keep only one single source of truth in your article entity reducer.

In your component you can simply map entities + recommended /articlesByAuthor to present the entity.

Disclaimer: I can recommend a blog post I wrote, which shows how a real world app uses normalizr to prevent problems in state management: Redux Normalizr: Improve your State Management

Siderolite answered 29/7, 2016 at 20:4 Comment(0)
H
2

I've implemented a small deviation of a generic reducer which can be found over the internet. It is capable of deleting items from cache. All you have to do is make sure that on each delete you send an action with deleted field:

export default (state = entities, action) => {
    if (action.response && action.response.entities)
        state = merge(state, action.response.entities)

    if (action.deleted) {
        state = {...state}

        Object.keys(action.deleted).forEach(entity => {
            let deleted = action.deleted[entity]

            state[entity] = Object.keys(state[entity]).filter(key => !deleted.includes(key))
                .reduce((p, id) => ({...p, [id]: state[entity][id]}), {})
        })
    }

    return state
}

usage example in action code:

await AlarmApi.remove(alarmId)

dispatch({
    type: 'ALARM_DELETED',
    alarmId,
    deleted: {alarms: [alarmId]},
})
Haywire answered 14/12, 2016 at 14:13 Comment(0)
S
1

Years late to the party, but here goes —

You can easily manage normalized reducer state with no boilerplate by using normalized-reducer. You pass in a schema describing the relationships, and it gives you back the reducer, actions, and selectors to manage that slice of state.

import makeNormalizedSlice from 'normalized-reducer';

const schema = {
  user: {
    articles: {
      type: 'article', cardinality: 'many', reciprocal: 'author'
    }
  },
  article: {
    author: {
      type: 'user', cardinality: 'one', reciprocal: 'articles'
    }
  }
};

const {
  actionCreators,
  selectors,
  reducer,
  actionTypes,
  emptyState
} = makeNormalizedSlice(schema);

The actions allow you to do basic CRUD logic as well as more complex ones such as relational attachments/detachments, cascading deletion, and batch actions.

Continuing the example, the state would look like:

{
  "entities": {
    "user": {
      "1": { 
        "id": "1", 
        "name": "Dan",
        "articles": ["1", "2"]
      }
    },
    "article": {
      "1": { 
        "id": "1",
        "author": "1",
        "title": "Some Article",
      },
      "2": {
        "id": "2",
        "author": "1",
        "title": "Other Article",
      }
    }
  },
  "ids": {
    "user": ["1"],
    "article": ["1", "2"]
  }
}

Normalized Reducer also integrates with normalizr:

import { normalize } from 'normalizr'
import { fromNormalizr } from 'normalized-reducer'

const denormalizedData = {...}
const normalizrSchema = {...}

const normalizedData = normalize(denormalizedData, normalizrSchema);
const initialState = fromNormalizr(normalizedData);

Another example of normalizr integration

Shophar answered 18/4, 2020 at 20:22 Comment(0)
S
0

In your reducer, keep a copy of the un-normalized data. This way, you can do something like this (when adding a new object to an array in state):

case ACTION:
  return {
    unNormalizedData: [...state.unNormalizedData, action.data],
    normalizedData: normalize([...state.unNormalizedData, action.data], normalizrSchema),
  }

If you do not want to keep un-normalized data in your store, you can also use denormalize

Spain answered 29/8, 2017 at 20:12 Comment(4)
Major red flags here. Firstly, you should refrain from duplicate data in the store. It's asking for trouble and is a code smell. Additionally, reducers should be as lean as possible and calling normalize on each cycle is not a recommend usage.Whilst
How would you recommend updating/ deleting when you are normalizing with a complex schema. For example, idAttribute is a function and process and merge strategies are used? This approach has been extremely simple and straightforward and never caused any perf issues for me.Spain
If you make modifications to the normalized data, now the denormalized, duplicate data ("unNormalizedData") is out of date.Whilst
I'd recommend following the standard of storing flat, normalized data and updating that in the reducers. And then using denormalize() in your UI components.Whilst

© 2022 - 2024 — McMap. All rights reserved.