Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vuex: Can't change deeply nested state data inside actions

In the store, I have an action to update some data, the action looks like this:


setRoomImage({ state }, { room, index, subIndex, image }) {
      state.fullReport.rooms[room].items[index].items[subIndex].image = image;
      console.log(state.fullReport.rooms[room].items[index].items[subIndex])
    },

Because all of this data is dynamic so I have to dynamically change the nested values and can't directly hard code the properties. The data looks like this:

fullreport: {
    rooms: {
        abc: {
          items: [
            {
              type: "image-only",
              items: [
                {
                  label: "Main Image 1",
                  image: ""
                },
                {
                  label: "Main Image 2",
                  image: ""
                }
              ]
            }
          ]
        }
      }
}

When I dispatch the action, In the console I can see that the value of the sub-property image is successfully mutated, but if I access the VueX store from the Vue DevTools inside Chrome, I see that value doesn't change there. Here is the console output:

enter image description here

Please, can somebody tell why is it happening? As I know that data is successfully changing, but somehow the state isn't showing it and hence my components do not rerender.

I also tried using Vue.set instead of simple assignment, but still no luck :(

Vue.set(
  state.fullReport.rooms[room].items[index].items[subIndex],
  "image",
   image
 );

Edit:

Following David Gard's answer, I tried the following:

I am also using Lodash _ (I know making whole copies of objects isn't good), this is the mutation code block.

let fullReportCopy = _.cloneDeep(state.fullReport);
fullReportCopy.rooms[room].items[index].items[subIndex].image = image;
Vue.set(state, "fullReport", fullReportCopy);

Now In the computed property, where the state.fullReport is a dependency, I have a console.log which just prints out a string whenever the computed property is re-computed.

Every time I commit this mutation, I see the computed property logs the string, but the state it is receiving still doesn't change, I guess Vue.set just tells the computed property that the state is changed, but it doesn't actually change it. Hence there is no change in my component's UI.

like image 749
Nadir Abbas Avatar asked Sep 12 '19 12:09

Nadir Abbas


1 Answers

As mentioned in comments - it quickly gets complicated if you hold deeply nested state in your store.

The issue is, that you have to fill Arrays and Objects in two different ways, hence, consider whether you need access to their native methods or not. Unfortunately Vuex does not support reactive Maps yet.

That aside, I also work with projects that require the dynamic setting of properties with multiple nested levels. One way to go about it is to recursively set each property.

It's not pretty, but it works:

function createReactiveNestedObject(rootProp, object) {
// root is your rootProperty; e.g. state.fullReport
// object is the entire nested object you want to set

  let root = rootProp;
  const isArray = root instanceof Array;
  // you need to fill Arrays with native Array methods (.push())
  // and Object with Vue.set()

  Object.keys(object).forEach((key, i) => {
    if (object[key] instanceof Array) {
      createReactiveArray(isArray, root, key, object[key])
    } else if (object[key] instanceof Object) {
      createReactiveObject(isArray, root, key, object[key]);
    } else {
      setReactiveValue(isArray, root, key, object[key])
    }
  })
}

function createReactiveArray(isArray, root, key, values) {
  if (isArray) {
    root.push([]);
  } else {
    Vue.set(root, key, []);
  }
  fillArray(root[key], values)
}

function fillArray(rootArray, arrayElements) {
  arrayElements.forEach((element, i) => {
    if (element instanceof Array) {
      rootArray.push([])
    } else if (element instanceof Object) {
      rootArray.push({});
    } else {
      rootArray.push(element);
    }
    createReactiveNestedFilterObject(rootArray[i], element);
  })
}

function createReactiveObject(isArray, obj, key, values) {
  if (isArray) {
    obj.push({});
  } else {
    Vue.set(obj, key, {});
  }
  createReactiveNestedFilterObject(obj[key], values);
}

function setValue(isArray, obj, key, value) {
  if (isArray) {
    obj.push(value);
  } else {
    Vue.set(obj, key, value);
  }
}

If someone has a smarter way to do this I am very keen to hear it!

Edit:

The way I use the above posted solution is like this:

// in store/actions.js

export const actions = {
  ...
  async prepareReactiveObject({ commit }, rawObject) {
    commit('CREATE_REACTIVE_OBJECT', rawObject);
  },
  ...
}

// in store/mutations.js
import { helper } from './helpers';

export const mutations = {
  ...
  CREATE_REACTIVE_OBJECT(state, rawObject) {
    helper.createReactiveNestedObject(state.rootProperty, rawObject);
  },
  ...
}

// in store/helper.js

// the above functions and

export const helper = {
  createReactiveNestedObject
}
like image 151
MarcRo Avatar answered Nov 15 '22 02:11

MarcRo