Search code examples
reactjsstateimmer.js

How to organise my nested object for better state management?


This is more of an organisation than technical question. I think I may be adding complexity, where a more experienced dev would simplify. I lack that experience, and need help.

It's a menu editor, where I load a menu object from my database into state:

state = {
  user_token: ####,
  loadingMenu: true,
  menu: {} // menu will be fetched into here
}

The object looks like this:

{
  menuID: _605c7e1f54bb42972e420619,
  brandingImg: "",
  specials: "2 for 1 drinks",
  langs: ["en", "es"],
  items: [
    {
      id: 0,
      type: "menuitem",
      isVisible: true,
      en: {
        name: "sandwich 1",
        desc: "Chicken sandwish"
      },
      es: {
        name: "torta 1"
      }, 
      price: 10
    },
  // ...
  // ABOUT 25 MORE ITEMS
  ]
}

The UI allows user to click on and update the items individually. So when they change the text I find myself having to do weird destructuring, like this:

function reducer(state, action) {
  if (action.type === UPDATE_NAME) {    
    const newMenuItems = state.menu.items.map((oldItem) => {
      if (oldItem.id === action.payload.id) {
        return { ...oldItem, ["en"]: { ...oldItem["en"], name: action.payload.newName } }
        // ["en"] for now, but will be dynamic later
      }
      return oldItem
    })

    return { ...state, menu: { ...state.menu, items: newMenuItems } }

  }
}

This seems like a a bad idea, because I'm replacing the entirety of state with my new object. I'm wondering if there is a better way to organize it?

I know there are immutability managers, and I tried to use immer.js, but ran into an obstacle. I need to map through all my menu items to find the one user wants to edit (matching the ID to the event target's ID). I don't know how else to target it directly, and don't know how to do this:

draft.menu.items[????][lang].name = "Sandwich One"

So again, I'm thinking that my organisation is wrong, as immutability managers should probably make this easy. Any ideas, what I can refactor?


Solution

  • First of all, your current reducer looks fine. That "weird destructuring" is very typical. You will always replace the entirety of state with a new object, but you are dealing with shallow copies so it's not an entirely new object at every level. The menu items which you haven't modified are still references to the same objects.


    I need to map through all my menu items to find the one user wants to edit (matching the ID to the event target's ID). I don't know how else to target it directly.

    You would use .findIndex() to get the index of the item that you want to update.

    const {lang, name, id} = action.payload;
    const index = draft.menu.items.findIndex( item => item.id === id);
    if ( index ) { // because there could be no match
      draft.menu.items[index][lang].name = name;
    }
    

    This is more of an organisation than technical question. I think I may be adding complexity, where a more experienced dev would simplify. I lack that experience, and need help.

    My recommendation for the state structure is to store all of the items in a dictionary keyed by id. This makes it easier to update an item because you no longer need to find it in an array.

    const {lang, name, id} = action.payload;
    draft.items[index][lang].name = name;
    

    The menu object would just have an array of ids instead of an array of objects for the items property. When you select a menu, your selector can replace the ids with their objects.

    const selectMenu = (state) => {
      const menu = state.menu;
      return { ...menu, items: menu.items.map((id) => state.items[id]) };
    };