vuex - is it possible to directly change state, even if not recommended? - vue.js

The documentation here
says,
You cannot directly mutate the store's state. The only way to change a store's state is by explicitly committing mutations.
My question is, is that good practice, or is that how the internals of the Vuex state works? In other words, is the Vuex state reactive in the same way Vue data is (it converts the JS object to an observable), or is it something else?
A similar question - could you directly change the state in an action instead of creating a mutation? I know it's bad practice and it loses some of the traceability that following the conventions gives - but does it work?

Could you directly change the state in an action instead of creating a mutation? I know it's bad practice and it loses some of the traceability that following the conventions gives - but does it work?
Works, but throws a warning AND an error.
vue.js:584 [Vue warn]: Error in callback for watcher "function () { return this._data.$$state }": "Error: [vuex] Do not mutate vuex store state outside mutation handlers."
(found in <Component>)
warn # vue.js:584
...
vue.js:1719 Error: [vuex] Do not mutate vuex store state outside mutation handlers.
at assert (VM260 vuex.js:103)
who knows what else might be broken after this.
See for yourself (notice the data updates in the template):
const store = new Vuex.Store({
strict: true,
state: {
people: []
},
mutations: {
populate: function (state, data) {
//Vue.set(state, 'people', data);
}
}
});
new Vue({
store,
el: '#app',
mounted: function() {
let self = this;
this.$http.get('https://api.myjson.com/bins/g07qh').then(function (response) {
// setting without commit
Vue.set(self.$store.state, 'people', response.data);
//self.$store.commit('populate', response.data)
}).catch(function (error) {
console.dir(error);
});
},
computed: {
datadata: function() {
return this.$store.state.people
}
},
})
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
<script src="https://unpkg.com/vue-resource"></script>
<div id="app">
Data: {{ datadata }}
</div>
the Vuex state reactive in the same way Vue data is (it converts the js object to an observable), or is it something else?
Yes. Actually, that's Vue itself that makes the store objects reactive. From the Mutations official docs:
Mutations Follow Vue's Reactivity Rules
Since a Vuex store's state is made reactive by Vue, when we mutate the
state, Vue components observing the state will update automatically.
This also means Vuex mutations are subject to the same reactivity
caveats when working with plain Vue:
Prefer initializing your store's initial state with all desired fields upfront.
When adding new properties to an Object, you should either:
Use Vue.set(obj, 'newProp', 123), or
Replace that Object with a fresh one. For example, using the stage-3 object spread
syntax we can
write it like this:
state.obj = { ...state.obj, newProp: 123 }
So even within mutations code, if you overwrite observables or create new properties directly (by not calling Vue.set(obj, 'newProp', newValue)), the object won't be reactive.
Follow up questions from comments (good ones!)
So it seems the observable object is slightly different than the regular Vue data - changes are only allowed to happen from a mutation handler. Is that right?
They could be, but I don't believe they are. The docs and evidences (see below vm.$watch discussion below) point torward they being exactly the same as data objects, at least with regards to reaction/observable behaviors.
How does the object "know" it was mutated from a different context?
This is a good question. Allow me to rephrase it:
If calling Vue.set(object, 'prop', data); from within Vue throws an exception (see demo above), why calling Vue.set(object, 'prop', data); from within a mutation function doesn't?
The answer lies within Store.commit()'s code. It executes the mutation code through a _withCommit() internal function.
All that this _withCommit() does is it sets a flag this._committing to true and then executes the mutation code (and returns _committing to false after the exection).
The Vuex store is then watching the states' variables and if it notices (aka the watcher triggers) that the variable changed while the _committing flag was false it throws the warning.
(Bonus: do notice that vuex uses vm.$watch --see Vue's vm.$watch API docs if you are not familiar with it -- to observe the variables, another hint that state's objects are the same as data objects - they rely on Vue's internals.)
Now, to prove my point, let's "trick" vuex by setting state._committing to true ourselves and then call Vue.set() from outside a mutator. As you can see below, no warning is triggered. Touché.
const store = new Vuex.Store({
strict: true,
state: {
people: []
},
mutations: {
populate: function (state, data) {
//Vue.set(state, 'people', data);
}
}
});
new Vue({
store,
el: '#app',
mounted: function() {
let self = this;
this.$http.get('https://api.myjson.com/bins/g07qh').then(function (response) {
// trick the store to think we're using commit()
self.$store._committing = true;
// setting without commit
Vue.set(self.$store.state, 'people', response.data);
// no warning! yay!
}).catch(function (error) {
console.dir(error);
});
},
computed: {
datadata: function() {
return this.$store.state.people
}
},
})
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
<script src="https://unpkg.com/vue-resource"></script>
<div id="app">
Data: {{ datadata }}
</div>

I am going to make this very simple:
Because the state object is already reactive, you can completely avoid using getters and mutations. All of Vue’s templates, computed, watch, etc. will continue to work the same as if using a component’s data. The store’s state acts as a shared data object.
But by doing so you will lose the ability to implement time-travel debugging, undo/redo, and setting breakpoints, because you will have circumvented the command design pattern and encapsulation of a member by using methods.

Related

Vuex freezing when modifying reactive state properties

I am fairly new to Vuex, and have ran into a problem I can't diagnose. My store is set up similarly to the Shopping example, and I've included the relevant module below.
The INIT action is called when the app loads, and everything functions fine. The LOOKUP action is later called from components, but freezes when calling the define mutation.
The current code is after trying several workarounds. Ultimately I'm trying to access state.pages from a component. I thought that the problem could've been because state.pages is an Object, so I made it non-reactive, and tried to make the component watch for changes in the pageCounter to retrieve the new page, but that didn't work.
I can include any other relevant information.
EDIT: Simplified the code to show more specific what the problem is.
store/modules/flashcards.js
// initial state
const state = () => ({
counter: 0,
})
// actions
const actions = {
}
// mutations
const mutations = {
increaseCounter(state) {
console.log(state.counter)
state.counter++; <----------- Code stops here
console.log(state.counter)
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
The component that accesses the store:
<template>
<div>
<md-button #click='increaseCounter'>Test</md-button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
import FlashCardComponent from './FlashcardComponent'
export default {
computed: {
...mapState({
counter: state => state.flashcards.counter
})
},
methods: {
...mapMutations('flashcards', ['increaseCounter']),
</script>
In increaseCounter, the first console.log(state.counter) is printed, but the second one isn't. This is a very simple access pattern, so I would appreciate insight into why it's giving this error.
I figured out the issue. The strict flag was being used when creating the Store object. Everything worked once I removed that. As Tordek said, don't do this.
I suppose I need to figure out why that's a problem, as it points to the state being manipulated outside of a mutation.

vue - changing localstate triggers vuex error

Whenever i update local state, vuex throws error [vuex] do not mutate vuex store state outside mutation handlers. even though I am not even trying to change vuex store. What is the problem?
data: {
selected: []
},
methods: {
addItem(item){
this.selected.push({
name: item.name,
count: item.count
})
},
applySelected(){
this.$store.dispatch('changeItems', this.selected)
}
}
<button #click="item.count++"/>
<span>{{item.count}}</span>
<button #click="item.count--"/>
items are in loop but proper markup and surrounding code is unnecessary for that example.
Now when selected[] is empty, when i do addItem(item) it works fine. Then when i change count, it still works just fine. When i commit changed to the store, guess what - it still works fine. But when i try to change count after it was submitted to the store, even though i have 0 getters, not reading store at all, don't have any additional mutation/dispatch calls, whenever i try to change count of selected[] items, it throws vuex error. but why?
it doesnt make any sense to change vuex state in component. its better to change it via a mutation.
but there is a way to solve this locally and you have to call a function that returns new object of items in vuex store. maybe like this:
computed: {
selected() {
return () => this.$store.state.selected
}
}

vue.js: scoping custom option merge strategies instead going global

Good Day Fellows,
Quick summary: how can I use custom option merge strategies on an individual basis per component and not globaly?
My problem:
I am extending my components via Mixins and it is working great so far. However, while it is working great with the likes of component methods, I often need to override some lifecycle hooks, like mounted, created, etc. The catch is, Vue - by default - queues them up in an array and calls them after another. This is of course defined by Vues default merge strategies.
However in some specific cases I do need to override the hook and not have it stack. I know I can customize Vue.config.optionMergeStrategies to my liking, but I want the mergeStrategy customized on a per component basis and not applying it globably.
My naive approach on paper was to create a higher function which stores the original hooks, applies my custom strategy, calls my component body and after that restores Vues original hooks.
Let's say like this
export default function executeWithCustomMerge(fn) {
const orig = deep copy Vue.config.optionMergeStrategies;
Vue.config.optionMergeStrategies.mounted = (parent, child) => [child];
fn();
Vue.config.optionMergeStrategies = deep copy orig;
}
And here's it in action
executeWithCustomMerge(() => {
Vue.component('new-comp', {
mixins: [Vue.component("old-comp")],
},
mounted() {
//i want to override my parent thus I am using a custom merge strategy
});
});
Now, this is not going to work out because restoring the original hook strategies still apply on a global and will be reseted before most hooks on my component are being called.
I wonder what do I need to do to scope my merge strategy to a component.
I had a look at optionMergeStrategies in more detail and found this interesting quote from the docs (emphasis mine):
The merge strategy receives the value of that option defined on the parent and child instances as the first and second arguments, respectively. The context Vue instance is passed as the third argument.
So I thought it would be straightforward to implement a custom merging strategy that inspects the Vue instance and looks at its properties to decide which strategy to use. Something like this:
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal, vm) {
if (vm.overrideCreated) {
// If the "overrideCreated" prop is set on the component, discard the mixin's created()
return [vm.created];
}
return mergeCreatedStrategy(toVal, fromVal, vm);
};
It turns out though that the 3rd argument (vm) is not set when the strategy function is called for components. It's a new bug! See https://github.com/vuejs/vue/issues/9623
So I found another way to inform the merge strategy on what it should do. Since JavaScript functions are first-class objects, they can have properties and methods just like any other object. Therefore, we can set a component's function to override its parents by setting a property on it and looking for its value in the merge strategy like so:
Vue.mixin({
created() {
this.messages.push('global mixin hook called');
}
});
const mixin = {
created() {
this.messages.push('mixin hook called');
},
};
const mergeCreatedStrategy = Vue.config.optionMergeStrategies.created;
Vue.config.optionMergeStrategies.created = function strategy(toVal, fromVal) {
if (fromVal.overrideOthers) {
// Selectively override hooks injected from mixins
return [fromVal];
}
return mergeCreatedStrategy(toVal, fromVal);
};
const app = {
el: '#app',
mixins: [mixin],
data: { messages: [] },
created() {
this.messages.push('component hook called');
},
};
// Comment or change this line to control whether the mixin created hook is applied
app.created.overrideOthers = true;
new Vue(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>Messages from hooks</h1>
<p v-for="message in messages">{{ message }}</p>
</div>

What is the main difference between computed and mounted in vue.js 2?

when i add computed() instead mounted() it throws an error
export default {
components: {
MainLayout
},
mounted(){
var x = document.getElementById('homeTabPanes');
// x.style.background = "blue";
console.log("check the value of x", x);
}
}
computed is an object containing methods that returns data, mounted is a life hook executed after the instance gets mounted, check out the links to the docs it have really good explanation
From the docs
..computed properties are cached based on their dependencies. A computed property will only re-evaluate when some of its dependencies have changed.
If you want data to be cached use Computed properties on the other hand mounted is a lifecycle hook, a method which is called as soon as the Vue instance is mounted on the DOM.
In-template expressions are very convenient, but they are meant for simple operations. Putting too much logic in your templates can make them bloated and hard to maintain.
That’s why for any complex logic, you should use a computed property.
Basic Example
<div id="reverseMessageContainer">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>
look at the js below:
var vm = new Vue({
el: '#reverseMessageContainer',
data: {
message: 'Hello'
},
computed: {
// a computed getter
reversedMessage: function () {
// `this` points to the vm instance
return this.message.split('').reverse().join('')
}
}
})
Here we have declared a computed property reversedMessage. The function we provided will be used as the getter function for the property vm.reversedMessage:
You can open the console and play with the example vm yourself. The value of vm.reversedMessage is always dependent on the value of vm.message.
console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'
For more better understanding you can visit
https://v2.vuejs.org/v2/guide/computed.html

vuejs vuex use state in two way binding without mutation (v-model)

I know that I am supposed to use mutations to change state. However I was wondering if it is theoretivally possible to use state in a v-model binding.
My current solution:
html:
...
<input v-model='todo'>
...
with mutation:
...
computed: {
todo: {
get () { return this.$store.state.todos.todo },
set (value) { this.$store.commit('updateTodo', value) }
}
}
...
without mutation
...
computed: {
todo: {
get () { return this.$store.state.todos.todo },
set (value) { this.$store.state.todos.todo = value }
}
}
...
what I would like:
...
<input v-model='this.$store.state.todos.todo'>
...
You can directly bind a Vuex state property to a component or input via v-model:
<input v-model='$store.state.todos.todo'>
But this is strongly recommended against. Vuex will warn you that you are mutating the state outside of a mutation function.
Since, when using Vuex, your state object is your source of truth which is designed to only be updated in a mutation function, it will quickly become hard to debug why the global state is changing if one component is affecting the global state without calling a mutation.
Most people, I believe, would recommend using your computed todo property example with mutations for the scenario you're describing.