what is vuex-router-sync for? - vuejs2

As far as I know vuex-router-sync is just for synchronizing the route with the vuex store and the developer can access the route as follows:
store.state.route.path
store.state.route.params
However, I can also handle route by this.$route which is more concise.
When do I need to use the route in the store, and what is the scenario in which I need vuex-router-sync?

Here's my two cents. You don't need to import vuex-router-sync if you cannot figure out its use case in your project, but you may want it when you are trying to use route object in your vuex's method (this.$route won't work well in vuex's realm).
I'd like to give an example here.
Suppose you want to show a message in one component. You want to display a message like Have a nice day, Jack in almost every page, except for the case that Welcome back, Jack should be displayed when the user's browsing top page.
You can easily achieve it with the help of vuex-router-sync.
const Top = {
template: '<div>{{message}}</div>',
computed: {
message() {
return this.$store.getters.getMessage;
}
},
};
const Bar = {
template: '<div>{{message}}</div>',
computed: {
message() {
return this.$store.getters.getMessage;
}
}
};
const routes = [{
path: '/top',
component: Top,
name: 'top'
},
{
path: '/bar',
component: Bar,
name: 'bar'
},
];
const router = new VueRouter({
routes
});
const store = new Vuex.Store({
state: {
username: 'Jack',
phrases: ['Welcome back', 'Have a nice day'],
},
getters: {
getMessage(state) {
return state.route.name === 'top' ?
`${state.phrases[0]}, ${state.username}` :
`${state.phrases[1]}, ${state.username}`;
},
},
});
// sync store and router by using `vuex-router-sync`
sync(store, router);
const app = new Vue({
router,
store,
}).$mount('#app');
// vuex-router-sync source code pasted here because no proper cdn service found
function sync(store, router, options) {
var moduleName = (options || {}).moduleName || 'route'
store.registerModule(moduleName, {
namespaced: true,
state: cloneRoute(router.currentRoute),
mutations: {
'ROUTE_CHANGED': function(state, transition) {
store.state[moduleName] = cloneRoute(transition.to, transition.from)
}
}
})
var isTimeTraveling = false
var currentPath
// sync router on store change
store.watch(
function(state) {
return state[moduleName]
},
function(route) {
if (route.fullPath === currentPath) {
return
}
isTimeTraveling = true
var methodToUse = currentPath == null ?
'replace' :
'push'
currentPath = route.fullPath
router[methodToUse](route)
}, {
sync: true
}
)
// sync store on router navigation
router.afterEach(function(to, from) {
if (isTimeTraveling) {
isTimeTraveling = false
return
}
currentPath = to.fullPath
store.commit(moduleName + '/ROUTE_CHANGED', {
to: to,
from: from
})
})
}
function cloneRoute(to, from) {
var clone = {
name: to.name,
path: to.path,
hash: to.hash,
query: to.query,
params: to.params,
fullPath: to.fullPath,
meta: to.meta
}
if (from) {
clone.from = cloneRoute(from)
}
return Object.freeze(clone)
}
.router-link-active {
color: red;
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>
<div id="app">
<p>
<router-link to="/top">Go to Top</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<router-view></router-view>
</div>
fiddle here
As you can see, the components are well decoupled from vuex and vue-router's logic.
This pattern sometimes works really effectively for the case that you're not concerned about the relationship between current route and the value returned from vuex's getter.

I saw this thread when I was learning Vue. Added some of my understanding on the question.
Vuex defines a state management pattern for Vue applications. Instead of defining component props and passing the shared state through props in all the places, we use a centralized store to organize the state shared by multiple components. The restriction on state mutation makes the state transition clearer and easier to reason about.
Ideally, we should get / build consistent (or identical) views if the provided store states are the same. However, the router, shared by multiple components, breaks this. If we need to reason about why the page is rendered like it is, we need to check the store state as well as the router state if we derive the view from the this.$router properties.
vuex-router-sync is a helper to sync the router state to the centralized state store. Now all the views can be built from the state store and we don't need to check this.$router.
Note that the route state is immutable, and we should "change" its state via the $router.push or $router.go call. It may be helpful to define some actions on store as:
// import your router definition
import router from './router'
export default new Vuex.Store({
//...
actions: {
//...
// actions to update route asynchronously
routerPush (_, arg) {
router.push(arg)
},
routerGo (_, arg) {
router.go(arg)
}
}
})
This wraps the route updates in the store actions and we can completely get rid of the this.$router dependencies in the components.

Related

Sendbird - Nuxt -Vuex - Do not mutate vuex store state outside mutation handlers [duplicate]

Why do I get this error:
Error [vuex] Do not mutate vuex store state outside mutation handlers.
What does it mean?
It happens when I try to type in the edit input file.
pages/todos/index.vue
<template>
<ul>
<li v-for="todo in todos">
<input type="checkbox" :checked="todo.done" v-on:change="toggle(todo)">
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button class="destroy" v-on:click="remove(todo)">delete</button>
<input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" #blur="doneEdit(todo)" #keyup.enter="doneEdit(todo)" #keyup.esc="cancelEdit(todo)">
</li>
<li><input placeholder="What needs to be done?" autofocus v-model="todo" v-on:keyup.enter="add"></li>
</ul>
</template>
<script>
import { mapMutations } from 'vuex'
export default {
data () {
return {
todo: '',
editedTodo: null
}
},
head () {
return {
title: this.$route.params.slug || 'all',
titleTemplate: 'Nuxt TodoMVC : %s todos'
}
},
fetch ({ store }) {
store.commit('todos/add', 'Hello World')
},
computed: {
todos () {
// console.log(this)
return this.$store.state.todos.list
}
},
methods: {
add (e) {
var value = this.todo && this.todo.trim()
if (value) {
this.$store.commit('todos/add', value)
this.todo = ''
}
},
toggle (todo) {
this.$store.commit('todos/toggle', todo)
},
remove (todo) {
this.$store.commit('todos/remove', todo)
},
doneEdit (todo) {
this.editedTodo = null
todo.text = todo.text.trim()
if (!todo.text) {
this.$store.commit('todos/remove', todo)
}
},
cancelEdit (todo) {
this.editedTodo = null
todo.text = this.beforeEditCache
},
},
directives: {
'todo-focus' (el, binding) {
if (binding.value) {
el.focus()
}
}
},
}
</script>
<style>
.done {
text-decoration: line-through;
}
</style>
stores/todos.js
export const state = () => ({
list: []
})
export const mutations = {
add (state, text) {
state.list.push({
text: text,
done: false
})
},
remove (state, todo) {
state.list.splice(state.list.indexOf(todo), 1)
},
toggle (state, todo) {
todo.done = !todo.done
}
}
Any ideas how I can fix this?
It could be a bit tricky to use v-model on a piece of state that belongs to Vuex.
and you have used v-model on todo.text here:
<input class="edit" type="text" v-model="todo.text" v-todo-focus="todo == editedTodo" #blur="doneEdit(todo)" #keyup.enter="doneEdit(todo)" #keyup.esc="cancelEdit(todo)">
use :value to read value and v-on:input or v-on:change to execute a method that perform the mutation inside an explicit Vuex mutation handler
This issue is handled here: https://vuex.vuejs.org/en/forms.html
Hello I have get the same problem and solve it with clone my object using one of the following:
{ ...obj} //spread syntax
Object.assign({}, obj)
JSON.parse(JSON.stringify(obj))
For your code I think you need to replace this part
computed: {
todos () {
// console.log(this)
return this.$store.state.todos.list
}
}
With this
computed: {
todos () {
// console.log(this)
return {...this.$store.state.todos.list}
}
}
I don't make sure if this is the best way but hope this helpful for other people that have the same issue.
This error may come from the fact you shallow cloned an object.
Meaning that you've tried to copy an object but an object is not a primitive type (like String or Number), hence it's passed by reference and not value.
Here you think that you cloned one object into the other, while you are still referencing the older one. Since you're mutating the older one, you got this nice warning.
Here is a GIF from Vue3's documentation (still relevant in our case).
On the left, it's showing an object (mug) being not properly cloned >> passed by reference.
On the right, it's properly cloned >> passed by value. Mutating this one does not mutate the original
The proper way to manage this error is to use lodash, this is how to load it efficiently in Nuxt:
Install lodash-es, eg: yarn add lodash-es, this is an optimized tree-shakable lodash ES module
you may also need to transpile it in your nuxt.config.js with the following
build: {
transpile: ['lodash-es'],
}
load it into your .vue components like this
<script>
import { cloneDeep } from 'lodash-es'
...
const properlyClonedObject = cloneDeep(myDeeplyNestedObject)
...
</script>
Few keys points:
lodash is recommended over JSON.parse(JSON.stringify(object)) because it does handle some edge-cases
we only load small functions from lodash and not the whole library thanks to this setup, so there is no penalty in terms of performance
lodash has a lot of well battle-tested useful functions, which is heavily lacking in JS (no core library)
UPDATE: structuredClone is also native and quite performant if you're looking for a solution for a deep copy, bypassing the need for Lodash at all.
There is no headache if you can use lodash
computed: {
...mapState({
todo: (state) => _.cloneDeep(state.todo)
})
}
Just in case someone's still being troubled by this,
I got my code working by making a duplicate/clone of the store state.
In your case, try something like this...
computed: {
todos () {
return [ ...this.$store.state.todos.list ]
}
}
It's basically a spread operator which results in making a clone of the todos.list array. With that, you're not directly changing the values of your state, just don't forget commit so your mutations will be saved in the store.
export default new Vuex.Store({
...
strict: true
})
try to comment "strict"
If you are using Vuex Modules, you might bump into this error if your module's data property is an object, instead of a function that returns an object, and you are sharing this Module between more than one Store.
So instead of:
// In stores/YourModule.js
export default {
state: { name: 'Foo' },
}
Change it to:
// In stores/YourModule.js
export default {
state: () => {
return { name: 'Foo' };
},
}
This is actually documented here:
Sometimes we may need to create multiple instances of a module, for
example:
Creating multiple stores that use the same module (e.g. To avoid
stateful singletons in the SSR (opens new window)when the
runInNewContext option is false or 'once'); Register the same module
multiple times in the same store. If we use a plain object to declare
the state of the module, then that state object will be shared by
reference and cause cross store/module state pollution when it's
mutated.
This is actually the exact same problem with data inside Vue
components. So the solution is also the same - use a function for
declaring module state (supported in 2.3.0+):
If your data is an array with objects inside. Below snippet is the solution
const toyData = await this.$store.dispatch(
`user/fetchCoinToys`,
payload
)
const msgList = toyData.msglist.map((data) => {
return { ...data }
})
I had to add mutation and call it instead of setting directly.
wrong:
someAction({state, rootState}) {
state.someValue = true;
}
right:
mutations: {
...
setSomeValue(state, val) {
state.someValue = val;
},
...
}
...
someAction({state, commit, rootState}) {
commit('setSomeValue', true);
}
It is not your case but if someone is using typescript and is having the same problem, adding this: any as the first param in your method or somewhere else should fix the problem

Best solution for getting {id} from url?

I'm trying to create a feature where individuals can create Listings, and other users can submit an application. ATM I can't quite figure out how to get the ID from the URL of:
'http://localhost:3000/listings/2/'
I have a hunch that this needs Vue router and $route.params.id? Basically I want in my form.listing the ID of the listing in which users are applying to. In this case it would be '2'
My folder structure is .../listings/_id/index.vue
This is my backend for the user_applications model in DRF:
listing = models.ForeignKey(Listings, on_delete=models.CASCADE, default="1")
Here's the frontend: I was hoping I could use the props value in script, but it seems like it only works in template, oh well.
data: () => ({
form: {
role: null,
company: null,
status: "Pending"
// listing: listings.id
}
}),
props: ["listings"],
computed: {
...mapGetters(["loggedInUser"])
},
methods: {
submit() {
this.$axios
.post("/api/v1/apps/", this.form)
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
}
}
You are on the right track when thinking of using VueRouter. Lets say that your component name is Listing.
import VueRouter from 'vue-router';
import Listing form './path/to/ListingComponent.vue';
const router = new VueRouter({
routes: [
{
path: '/listing/:id',
component: Listing,
// return parms.id as listingId for the `Listing` component.
props: route => ({ listingId: route.params.id })
}
]
})
In this way you have listingId passed to the props of Listing.
Yes, it's as easy as this.$route.params.id. You can invoke $route.params.id directly in your template, without using this. Don't use arrow functions in your component, though! They strip out the value of this! Also, all props and data attributes are available on this as well. So this.listing and this.$route are available, not only in your template, but in your Vue component's methods and computed properties, as well.
Try this approach, instead:
data() {
return {
form: {
role: null,
company: null,
status: "Pending",
listing: this.listings.id
}
}
},

Transfer Data From One Component to Another

I have a component which makes a call to my backend API. This then provides me with data that I use for the component. I now want to create another component which also uses that data. While I could just do another api call that seems wasteful.
So, in Profile.vue i have this in the created() function.
<script>
import axios from 'axios';
import { bus } from '../main';
export default {
name: 'Profile',
data() {
return {
loading: false,
error: null,
profileData: null,
getImageUrl: function(id) {
return `http://ddragon.leagueoflegends.com/cdn/9.16.1/img/profileicon/` + id + `.png`;
}
}
},
beforeCreate() {
//Add OR Remove classes and images etc..
},
async created() {
//Once page is loaded do this
this.loading = true;
try {
const response = await axios.get(`/api/profile/${this.$route.params.platform}/${this.$route.params.name}`);
this.profileData = response.data;
this.loading = false;
bus.$emit('profileData', this.profileData)
} catch (error) {
this.loading = false;
this.error = error.response.data.message;
}
}
};
</script>
I then have another child component that I've hooked up using the Vue router, this is to display further information.
MatchHistory compontent
<template>
<section>
<h1>{{profileDatas.profileDatas}}</h1>
</section>
</template>
<script>
import { bus } from '../main';
export default {
name: 'MatchHistory',
data() {
return {
profileDatas: null
}
},
beforeCreate() {
//Add OR Remove classes and images etc..
},
async created() {
bus.$on('profileData', obj => {
this.profileDatas = obj;
});
}
};
</script>
So, I want to take the info and display the data that I have transferred across.
My assumption is based on the fact that these components are defined for two separate routes and an event bus may not work for your situation based on the design of your application. There are several ways to solve this. Two of them listed below.
Vuex (for Vue state management)
Any local storage option - LocalStorage/SessionStorage/IndexDB e.t.c
for more information on VueX, visit https://vuex.vuejs.org/.
for more information on Localstorage, visit https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage.
for more information on session storage, visit https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
The flow is pretty much the same for any of the options.
Get your data from an API using axios as you did above in Profile.vue
Store the retrieved data with VueX or Local/Session storage
Retrieve the data from Vuex or local/session storage in the created method of MatchHistory.vue component
For the local / session storage options, you will have to convert your object to a json string as only strings can be stored in storage. see below.
in Profile.vue (created)
const response = await axios.get(........)
if(response){
localStorage.setItem('yourstoragekey', JSON.stringify(response));
}
In MatchHistory.Vue (created)
async created() {
var profileData = localStorage.getItem('yourstoragekey')
if(profileData){
profileData = JSON.parse(profileData );
this.profileData = profileData
}
}
You can use vm.$emit to create an Eventbus
// split instance
const EventBus = new Vue({})
class IApp extends Vue {}
IApp.mixin({
beforeCreate: function(){
this.EventBus = EventBus
}
})
const App = new IApp({
created(){
this.EventBus.$on('from-mounted', console.log)
},
mounted(){
this.EventBus.$emit('from-mounted', 'Its a me! Mounted')
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
further readings
You can make use of the VUEX which is a state management system for Vue.
When you make api call and get the data you need, you can COMMIT a MUTATION and pass your data to it. What it will do, it will update your STATE and all of your components will have access to its state (data)
In your async created(), when you get response, just commit mutation to your store in order to update the state. (omitted example here as the vuex store will need configuration before it can perform mutations)
Then in your child component,
data(){
return {
profileDatas: null
}
},
async created() {
this.profileDatas = $store.state.myData;
}
It might seem like an overkill in your case, but this approach is highly beneficial when working with external data that needs to be shared across multiple components

Is it possible to use vue-router without a component in vue 2?

I have a very simple page, just a few lines of code and I would like to use vue-router without a vue-component.
I would like to get the string "my-search-query" after the root:
www.example.com/my-search-query
and use it in my main Vue() object. Is this possible?
Thank you!
It is possible to use vue-router without a vue-component and still be notified about path changes in the main Vue() app object:
You can get the "my-search-query" string from www.example.com/#/my-search-query like this:
const router = new VueRouter({
routes: [
{ path: '/:question' }
]
})
const app = new Vue({
el: '#app',
router,
watch: {
'$route.params.question': function(newVal, oldVal) {
alert(newVal === 'my-search-query');
}
}
});
Thanks to #samayo for pointing me in the right direction!
Since the router is part of your view model instance, you can create a computed function that returns the path value of the router. For example, you can do the following:
data: function() {
router: new VueRouter()
},
computed: function() {
routerPath: function() {
return this.router.currentRoute.path;
}
},
watch: function() {
routerPath: function(val) {
switch(val) {
case '/':
// #TODO: logic for home
break;
case '/path1':
var query = this.router.currentRoute.query;
// #TODO: logic form query params
break;
}
}
}
With this, the router will manage the history for you, but you don't have to divide your code up into components. For example, I have a pretty small page and just wanted to put everything into the same viewModel without breaking everything up into components.
As tom_h has said you can use window.location.pathname if you just want to get the path name. If you want to be notified when this pathname changes, then first initialize it to your data(){} object and watch it as:
data () {
return {
path: window.location.pathname
}
},
watch: {
path (newpath, oldpath) {
alert('location path just changed from ' + newpath + ' to ' + oldpath )
}
}

How to use Vue Router from Vuex state?

In my components I've been using:
this.$router.push({ name: 'home', params: { id: this.searchText }});
To change route. I've now moved a method into my Vuex actions, and of course this.$router no longer works. Nor does Vue.router. So, how do I call router methods from the Vuex state, please?
I'm assuming vuex-router-sync won't help here as you need the router instance.
Therefore although this doesn't feel ideal you could set the instance as a global within webpack, i.e.
global.router = new VueRouter({
routes
})
const app = new Vue({
router
...
now you should be able to: router.push({ name: 'home', params: { id: 1234 }}) from anywhere within your app
As an alternative if you don't like the idea of the above you could return a Promise from your action. Then if the action completes successfully I assume it calls a mutation or something and you can resolve the Promise. However if it fails and whatever condition the redirect needs is hit you reject the Promise.
This way you can move the routers redirect into a component that simply catches the rejected Promise and fires the vue-router push, i.e.
# vuex
actions: {
foo: ({ commit }, payload) =>
new Promise((resolve, reject) => {
if (payload.title) {
commit('updateTitle', payload.title)
resolve()
} else {
reject()
}
})
# component
methods: {
updateFoo () {
this.$store.dispatch('foo', {})
.then(response => { // success })
.catch(response => {
// fail
this.$router.push({ name: 'home', params: { id: 1234 }})
})
I a situation, I find myself to use .go instead of .push.
Sorry, no explanation about why, but in my case it worked. I leave this for future Googlers like me.
I believe rootState.router will be available in your actions, assuming you passed router as an option in your main Vue constructor.
As GuyC mentioned, I was also thinking you may be better off returning a promise from your action and routing after it resolves. In simple terms: dispatch(YOUR_ACTION).then(router.push()).
state: {
anyObj: {}, // Just filler
_router: null // place holder for router ref
},
mutations: {
/***
* All stores that have this mutation will run it
*
* You can call this in App mount, eg...
* mounted () {
* let vm = this
* vm.$store.commit('setRouter', vm.$router)
* }
*
setRouter (state, routerRef) {
state._router = routerRef
}
},
actions: {
/***
* You can then use the router like this
* ---
someAction ({ state }) {
if (state._router) {
state._router.push('/somewhere_else')
} else {
console.log('You forgot to set the router silly')
}
}
}
}
Update
After I published this answer I noticed that defining it the way I presented Typescript stopped detecting fields of state. I assume that's because I used any as a type. I probably could manually define the type, but it sounds like repeating yourself to me. That's way for now I ended up with a function instead of extending a class (I would be glad for letting me know some other solution if someone knows it).
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default (router: VueRouter) => {
return new Store({
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
import Vue from 'vue'
import App from './App.vue'
import createStore from './store'
// ...
new Vue({
router,
store: createStore(router),
render: createElement => createElement(App)
}).$mount('#app')
Initial answer content
I personally just made a wrapper for a typical Store class.
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default class extends Store<any> {
constructor (router: VueRouter) {
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
}
If You need $route You can just use router.currentRoute. Just remember You rather need router reactive if You want Your getters with router.currentRoute to work as expected.
And in "main.ts" (or ".js") I just use it with new Store(router).
import Vue from 'vue'
import App from './App.vue'
import Store from './store'
// ...
new Vue({
router,
store: new Store(router),
render: createElement => createElement(App)
}).$mount('#app')