AR7 AR7 - 4 months ago 120
React JSX Question

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

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?

AR7 AR7
Answer

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)
  };