Redux + Normalizr detached (deleted) operation
Asked Answered
I

3

11

I use redux with normalizr to normalize the response from server, basically follow the real-world example. This way entities reducer is very simple, just merge the response. The problem I have right now is kind of delete operation. I've found this issue#21 of normalizr repo but still couldn't figure out how to solve this. For example,

Current state is

{
  entities:
    product_categories: {
      ...
      13: {
        ...
        products: ["1", "2"], <--------------- [i] Current state
        ...
      }
    },
    products: {
      1: {
        id: "1"
      }
    }
}

The normalized response is

{
  ...
    product_categories: {
      ...
      13: {
        ...
        products: ["1"], <---------------- [2] Normalized result
      }
  ...
}

As you can see, the backend api just returns all product ids that belonged to this category, in this case "2" is detached. When 'entities' reducer merges the this response, "2" is still hanging around. Right now I just reload the page but i'm wondering if there's a better way to handle this case?

In entities reducer, I just merge it like in real-world example.

return merge({}, state, action.payload.entities);

Intercom answered 16/11, 2015 at 2:7 Comment(1)
This is a really good question. IMO it points out a substantial flaw in the whole Redux + Normalizr architecture. Sure you have a single root to state, but its not exactly a truthful representation. Requiring the use of delete flags is far too opinionatedNash
A
11

Just don't worry about it being there. Think of your state as of a database. You don't truly delete records from the database either to avoid complicated cascades—usually you just change their status in the database. Similarly, with Normalizer, instead of truly deleting the entities, let them be in cache until the user leaves the page!

Alduino answered 18/11, 2015 at 23:27 Comment(2)
What do you mean by, "you don't truly delete records from the database"?Nash
Instead of removing an item from an array, you add a flag to it named "deleted" or something. Then update your selector to items.filter(item => !item.deleted)Fahy
D
0

Below is an explanation of my solution followed by code.

To perform a delete I have updated my reducer to a handle a delete action: REMOVE_ENTITY_ITEM. In the action I pass in the id and name of the entity that is to be removed.

In the reducer I first delete the entity itself which is at store.entities[entityName][entityId]. Then next i need to remove its id from all other entities which might be referring to it. Since I am using normalizr all my entities are flat and if they refer to another entity then they will only have its id in an array. This makes it relatively straightforward to remove the reference. I just loop over all entities and filter out the reference to the entity being removed.

I use this approach in conjunction with the other two approaches of #1.) refreshing the app/state and #2.) flipping the entities status bit rather than deleting and then filtering the turned off items in the UI. These approaches have been well discussed here

const entities = (state={}, action) => {
  if(action.payload && action.payload.entities) {
    return merge( {} , state, action.payload.entities);
  }else{
    return deleteHandlingReducer(state, action)
  }
}

const deleteHandlingReducer = (state=initialSate, action) => {
  switch(action.type){
    case "REMOVE_ENTITY_ITEM":
      if (!action.meta || !action.meta.name || !action.meta.id) {
        return state;
      }else{
        let newState = Object.assign({}, state);
        if(newState[action.meta.name]){
          delete newState[action.meta.name][action.meta.id];
          Object.keys(state).map(key => {
            let entityHash = state[key];
            Object.keys(entityHash).map(entityId => {
              let entity = entityHash[entityId];
              if(entity[action.meta.name] &&
                Array.isArray(entity[action.meta.name])){
                  entity[action.meta.name] = entity[action.meta.name].
                    filter(item => item != action.meta.id)
              }
            });
          })
        }
        return newState;
      }
    default:
      return state;
  }
}

Now to delete i fire an action like this:

store.dispatch({
  type: "REMOVE_ENTITY_ITEM",
  meta: {
    id: 1,
    name: "job_titles"
  }
});
Deirdra answered 14/11, 2016 at 16:2 Comment(0)
L
0

Use lodash assign. If you use merge will not work.

Example:

const object = {
  'a': [1,2,3]
}

const other = {
  'a': [1,2]
}

// item 3 was not removed
const merge = _.merge({}, object, other)
console.log(merge) // {'a': [1,2,3]}

// item 3 was removed
const assign = _.assign({}, object, other)
console.log(assign) // {'a': [1,2]}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>

Here, how would your code look like

// state
const state = {
 entities: {
   product_categories: {
     '1': {
       id: 1,
       products: [1,2]
     }
   },
   products: {
     '1': {
       id: 1
     },
     '2': {
       id: 2
     }
   }
 }
}

// example of action
// normalized by normalizr
const action = {
  type: 'REMOVE_PRODUCT_CATEGORY_SUCCESS',
  payload: {
    entities: {
      product_categories: {
        '1': {
           id: 1,
           products: [1]
        }
      }
    }
  }
}

// product_categories entity reducer
function productCategoriesEntityReducer (state = {}, action) {
  switch (action.type) {

    default:
      if (_.has(action, 'payload.entities.product_categories')) {
        return _.assign({}, state, action.payload.entities.product_categories)
      }
      return state
  }
}

// run reducer on state.entities.product_categories
const newState = productCategoriesEntityReducer(state.entities.product_categories, action);
console.log(newState)
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
Labrum answered 19/4, 2018 at 13:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.