Skip to content

Instantly share code, notes, and snippets.

@DawidMyslak
Last active April 22, 2024 12:49
Show Gist options
  • Save DawidMyslak/2b046cca5959427e8fb5c1da45ef7748 to your computer and use it in GitHub Desktop.
Save DawidMyslak/2b046cca5959427e8fb5c1da45ef7748 to your computer and use it in GitHub Desktop.
Vue.js and Vuex - best practices for managing your state

Vue.js and Vuex - best practices for managing your state

Modifying state object

Example

If you have to extend an existing object with additional property, always prefer Vue.set() over Object.assign() (or spread operator).

Example below explains implications for different implementations.

const store = new Vuex.Store({
  state: {
    user: {
      id: 1
    }
  },
  getters: {
    getUserId: state => state.user.id,
    getUserName: state => state.user.name
  },
  mutations: {
    setUserName (state, name) {
      // 1st solution:
      // it won't trigger getters at all, more details:
      // https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
      state.user.name = name

      // 2nd solution:
      // same as "state.user = Object.assign({}, state.user, { name })"
      // it will trigger both getters every time when you call this mutation
      state.user = { ...state.user, name }

      // 3rd solution:
      // it will trigger both getters when you call this mutation for the first time,
      // but if you call it the next time only getUserName() will be triggered
      // Vue.set() will check if property already exists before re-assigning the entire object
      Vue.set(state.user, 'name', name)
    }  
  }
})

Ideally, try to initialize your state with all desired fields upfront, so in the example above state should be declared in the following way:

state: {
  user: {
    id: 1,
    name: null
  }
}

Summary

Be aware that adding a new property to an existing state object can have a significant impact on your application performance. It will trigger all getters based on that object (and potentially computed properties declared inside your componets).

When your application gets bigger over time and your state becomes more complex, try to use modules for better data encapsulation. So even if you have to add a new property at least it will have an impact only on a single module.

Same rules apply to nested objects and their properties, so prefer flat structure when you are designing your store schema.

Modifying state array

Example

If you have to add a new object to an existing array, always define all potential properties of the object upfront even if you do not know their values at the time.

Check the example below for more details.

const store = new Vuex.Store({
  state: {
    users: []
  },
  getters: {
    getUsersNames: state => {
      return state.users.map(user => user.name)
    }
  },
  mutations: {
    addUser (state, name) {
      // 1st solution:
      // it will work as expected and trigger getUsersNames()
      state.users.push({name})

      // 2nd solution
      // even though we are recreating the entire array,
      // it will behave in the same way as first solution
      // more details: https://vuejs.org/v2/guide/list.html#Replacing-an-Array
      state.users = [...state.users, {name}]
    },
    updateUserAvatar (state, {index, avatar}) {
      // 1st solution:
      // it will properly update an array item, but also it will trigger getUsersNames()
      Vue.set(state.users[index], 'avatar', avatar)

      // 2nd solution:
      // it's going to work only if we modify addUser() function and include avatar property there:
      // "state.users.push({name, avatar: null})"
      // the benefit of this approach is that getUsersNames() won't be triggered
      state.users[index].avatar = avatar
    }
  }
})

Summary

Be aware that adding a new item to an existing state array will always trigger all getters (and computed properties) which are based on that array. Same will happen if you remove the item.

When you add or remove items from an array it's okay to recreate the array entirely - feel free to use latest JavaScript features like spread operator or simply call array's mutation methods like push().

Different rules apply when you update existing object inside an array. First of all, declare all object properties (even with null values) before you add it to the array, they will become reactive properties observed by Vue. Avoid adding new properties to the object, it will always trigger all getters that are based on the array. You should modify object properites directly (for example state.users[0].name = "Test") and avoid recreating the entire array for updates.

@MartinMuzatko
Copy link

it is Object.assign not Object.assing :) Otherwise - nice guide!

@chanlito
Copy link

Awesome gist! Thanks.

@giovannefeitosa
Copy link

That's annoying to figure it out, your gist is a great help, thanks

@daniel-farina
Copy link

Hey thank you for the clean explanation.

Cheers

@msonsona
Copy link

msonsona commented Oct 8, 2018

Thanks for this gist.
Question: if we'd like to add a mutation to clear the users' array, what would the best option be:

    clearUsers (state) {
      // 1st solution
      state.users = []

      // 2nd solution
      state.users.splice(0)
    },

Thanks!

@lmiller1990
Copy link

@msonsona

Both of those will work in the same way! Take your pick.
Doing state.users = [] works, since you are replacing an array, as per the docs here.

splice is wrapped by Vue, as noted here.

@nagwan
Copy link

nagwan commented May 9, 2019

thank u, it really helps

@stevenaanen
Copy link

Thanks, great info!

What do you think about the pattern to not use arrays at all, but instead use object hashes (based on ID as the key of each member for instance) at all times? This is a common approach in Redux. I can imagine that this would trigger fewer getters in case one member of a list gets updated without affecting others (e.g. getters by ID for other members).

@DawidMyslak
Copy link
Author

What do you think about the pattern to not use arrays at all, but instead use object hashes (based on ID as the key of each member for instance) at all times?

Hey @stevenaanen, it's perfectly fine approach and I prefer it too. One additional cost is that you need to use Object.values if you want to get all items and iterate over them, but so far it's been working great for me.

@rabbygit
Copy link

If I want to update an existing object in a array what should be the best practice ?

@DawidMyslak
Copy link
Author

If I want to update an existing object in a array what should be the best practice ?

@rabbygit you can do something like this:

const state = {
  users: [{ id: 1, name: 'Old name' }]
};

const userIdToUpdate = 1;

// get a reference to the object
const user = state.users.find(u => u.id === userIdToUpdate);

// update the user object
user.name = 'New name';

@turarabu
Copy link

If I want to update an existing object in a array what should be the best practice ?

@rabbygit you can do something like this:

const state = {
  users: [{ id: 1, name: 'Old name' }]
};

const userIdToUpdate = 1;

// get a reference to the object
const user = state.users.find(u => u.id === userIdToUpdate);

// update the user object
user.name = 'New name';

He want to update an existing object in array, and not property of object in array

@jdeeline
Copy link

jdeeline commented Aug 5, 2020

If I want to update an existing object in a array what should be the best practice ?

const state = {
	users: [{ id: 1, name: 'John' }, { id: 2, name: 'Homer' }]
};

const userToUpdate = { id: 1, name: 'Alex' };

// get the index of an existing object in an array 
const index = state.users.findIndex(_ => _.id === userToUpdate.id);

// replace object in array
state.users[index] = userToUpdate;

@Goloburda
Copy link

@jdeeline, Did you test your code? Vuex won't trigger an UI update in your case.
You should add:

state.users[index] = userToUpdate; 
state.users = [...state.users]

to manually trigger the update.
Or you can use:

  updateTour: (state, response) => {
    const index = state.tours.findIndex(tour => tour.key === response.key)
    state.tours.splice(index, 1, response)
  }

@jdeeline
Copy link

jdeeline commented Aug 20, 2020

@Goloburda thank you for your solution.

The answer was to the question about an array of objects. Of course, if we are talking about Vuex, then mutation should be used.

...
state: {
    users: [
		{ id: 1, name: 'John' },
		{ id: 2, name: 'Homer' }
	]
},
mutations: {
    updateUser(state, user) {
        const index = state.users.findIndex(_ => _.id === user.id)
        state.users[index] = user
    }
}
...

It works, tested.

@naivenaive
Copy link

I have tried to fix this problem for one night. I can't figure it out until I read your explanation. It is really really helpful. Thanks.

@Darkzarich
Copy link

Darkzarich commented Sep 18, 2020

Could anyone say something about this method of update of an existing item in an array for Vuex. It seems to be working just fine.

const store = new Vuex.Store({
  state: {
    users: []
  },
  mutations: {
     UPDATE_USER (state, editedUser) {
         // find doesn't create a new object, it returns an existing one from users array and we can modify it
         const oldUser = state.users.find( user => user.id === editedUser.id );
         if (oldUser) {
            // not creating a new object but modifying old object here
            Object.assign(oldUser, editedUser)
         }
     },
     ADD_USER (state, user) {
         state.users.push(user);
     },
  }
})

In UPDATE_USER new fields defined in editedUser will be created in old user object from users array, old fields are updated with a new value if old fields of old user object were defined in editedUser and it's totally reactive. I would like to know caveats though.

@Goloburda @jdeeline just a note. While your approach will work in a simple case if you make the case harder like let's say we now have a modal window for editing users and your modal will get a user data like that:

... 

<li v-for="user in users" @click="openModal(user)"> {user.name} </li>
<Modal :user="editUser" :show="showModal" />
...

data: {
    editUser: null,
    showModal: false,
}
methods: {
    openModal (user) {
      this.editUser = user;
      this.showModal = true;
    }
}

After you update user in Vuex store its updated version will not be seen in modal because you replaced that object in Vuex store with a new one and editUser will not be updated, it will still contain the old object.

As workaround you can avoid replacing the entire object and just change each of its properties. Or you can instead of passing entire object in Modal component just pass let's say user.id and then add a computed field (and pass it in the modal window as well) which will search for that user via its user.id

@atilkan
Copy link

atilkan commented Feb 18, 2021

You may need to fix your example. And this is more like basic usage than best practices. It is clean though.

  • You can't pass third parameter on mutations/actions.
updateUserAvatar (state, index, avatar)
  • Getters without a formatting/filtering are waste of memory. You can directly access state.
getUserId: state => state.user.id,
getUserName: state => state.user.name

@pimvdh
Copy link

pimvdh commented May 8, 2021

I tried the code for the array state, without luck within my Vue / vuex project. I get an error on the return of the getUsersNames => "Property 'name' does not exist on type 'never'"

And an error on the line "state.users.push({name})" => "(property) name: any
Type 'any' is not assignable to type 'never'"

Is the example incomplete, causing this issues at my project, or do I need to change settings to make it work?

Any help is appreciated.

@galakhov
Copy link

galakhov commented May 8, 2021

I tried the code for the array state, without luck within my Vue / vuex project. I get an error on the return of the getUsersNames => "Property 'name' does not exist on type 'never'"

And an error on the line "state.users.push({name})" => "(property) name: any
Type 'any' is not assignable to type 'never'"

Hi,

  1. did you try to add some data into the state manually?
state: { users: [{ id: 1, name: 'some name' }] }
  1. Are you using TypeScript in your project?

@pimvdh
Copy link

pimvdh commented May 9, 2021

I tried the code for the array state, without luck within my Vue / vuex project. I get an error on the return of the getUsersNames => "Property 'name' does not exist on type 'never'"
And an error on the line "state.users.push({name})" => "(property) name: any
Type 'any' is not assignable to type 'never'"

Hi,

  1. did you try to add some data into the state manually?
state: { users: [{ id: 1, name: 'some name' }] }
  1. Are you using TypeScript in your project?
  1. When adding data manually this works okay, however the data should contain openLayers layers and I can't generate it within the stores "constructor" for the array. So I will add the data later was my thought. Is this a wrong idea?

  2. I am using typescript, how does this effect the problem?

Is this somehow

@galakhov
Copy link

galakhov commented May 9, 2021

  1. When adding data manually this works okay, however the data should contain openLayers layers and I can't generate it within the stores "constructor" for the array. So I will add the data later was my thought. Is this a wrong idea?
  2. I am using typescript, how does this effect the problem?

Is this somehow

  1. If you've got JSON file with openLayers/mapLayers and are using Vuex, you could import your data, for instance, with axios and save it to the store with a mutation function like it's shown in this post:
    https://stackoverflow.com/questions/56075612/how-do-i-use-json-data-with-vuex-store-no-webpack
  2. You've got type mismatch error (Type 'any' is not assignable to type 'never') in your first comment. Typescript itself adds additional overhead to your projects and complicates understanding / learning, especially for beginners. If you want to do it properly, you'd need to write your own Vuex types/interfaces for the store according to the structure of the data you're working with. Then you'd need to apply these types to getters, actions and mutations. It's probably a good idea to start with modules, as it's shown in this video.
    However, to inculde Vuex types there are lots of different approaches (see their code), each of which covers some percentage of the Vuex functionality, including the vuex-module-decorator/vue-property-decorator, which still tends to be the "cleanest" one.
    If you're just starting out I wouldn't recommend to use TypeScript within a Vue/Vuex (pet) project at all, unless you know what you're doing, or it's a big project with lots of developers, or an existing codebase with some strict predefined guidelines.

Good luck!

@pimvdh
Copy link

pimvdh commented May 10, 2021

@galakhov:
Many thanks, your responses helped me into the correct directions, and putting things in the correct perspective. I thought typescript would help me get things correctly structured, but probably I can better ignore this (for now).

@viniciusdeliz
Copy link

Thank you for this document. I suggest only a small correction: change 'Modyfing' instances to 'Modifying'. Cheers!

@hbackman
Copy link

Thanks for the awesome gist!

@stt106
Copy link

stt106 commented Oct 23, 2021

Hi, thanks for the gist which is very helpful. I am new to Vue and Vuex; got a question posted at https://stackoverflow.com/questions/69684791/nested-components-and-states-in-vue-with-vuex
Would you please take a look?
I am basically not sure how to handle state object having proper which is of type array, and such a property can have dynamic number of items depending on other state triggered from other component.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment