Is it possible to call Vuex mapActions from a separate module that's imported into a component?
I'm trying to standardize a set of functions in a vue.js web app. I'd like to import them into each component and pass some values the to function operate. I'm using vuex to manage state. Currently each component calls these functions each time they're loaded (identically the same).
I'd like to refactor into this into one module and import it into each component as needed. This code uses mapActions as part of it's function. Below are the relevant pieces of code: component, module, vuex action
Vue component:
//the imported function call
if (!this.queued){
timer.updatePage(this.pagination, this.orders);
}
the module code (advance.js):
import { mapActions } from 'vuex';
let currentComp = {
name: 'purchase',
date: null,
start: false
}
const timer = {
...mapActions(['currentComponent']),
updatePage(pagination, order) {
currentComp.name = 'nextComponent';
this.currentComponent(currentComp);
}
}
export default timer;
the vuex code:
//in the actions section:
currentComponent({
commit
}, comp) {
console.log(comp);
commit('setCurrentComponent', comp);
}
//in the mutations section:
setCurrentComponent: (state, comp) => {
state.currentComponent = comp.name;
return state;
}
when the component runs the imported function i get:
vuex.esm.js?2f62:870 Uncaught TypeError: Cannot read property 'dispatch' of undefined
at Object.mappedAction [as currentComponent] (vuex.esm.js?2f62:870)
at eval (advance.js?935c:37)
when i remove the this from this.currentComponent i get:
advance.js?935c:37 Uncaught ReferenceError: currentComponent is not defined
at eval (advance.js?935c:37)
thanks in advance for any guidance.
mapActions is a shortcut for creating a method that looks something like this
currentComponent() {
this.$store.dispatch('xxx')
}
When you call this function, the this context is timer. Since timer does not have a $store property, you get the error Cannot read property 'dispatch' of undefined. The quickest way to get around this would be to change the this context to the component which does have a $store property. You could do this by passing the component as a third property in updatePage and binding currentComponent to the function.
// component code
timer.updatePage(this.pagination, this.orders, this);
// advance.js
const timer = {
...mapActions(['currentComponent']),
updatePage(pagination, order, component) {
currentComp.name = 'nextComponent';
this.currentComponent.bind(component)(currentComp);
}
}
I'd recommend using a mixin for this type of behavior though.
import { mapActions } from 'vuex';
let currentComp = {
name: 'purchase',
date: null,
start: false
}
const timerMixin = {
methods: {
...mapActions(['currentComponent']),
updatePage(pagination, order) {
currentComp.name = 'nextComponent';
this.currentComponent(currentComp);
}
}
}
export default timerMixin;
In your component, import the timerMixin and register it as a mixin. Those methods would then be available directly on your component and you can call them with a small tweak to your existing code.
if (!this.queued){
this.updatePage(this.pagination, this.orders);
}
Related
I am using the PrimeVue component library and I try to call useToast() from inside my own custom composition function but it is throwing me this error:
[Vue warn]: inject() can only be used inside setup() or functional components.
Here is my composition function:
import axios from 'axios'
import { useToast } from 'primevue/usetoast'
export function useAxiosInterceptors() {
const addResponseInterceptors = () => {
axios.interceptors.response.use(
(error) => {
if (error.response?.status == 401) {
showErrorToast(
'Session expired',
'It looks like you do not have a valid access token.'
)
return Promise.reject(error)
}
)
}
const showErrorToast = (summary, detail) => {
const toast = useToast() // <-------- this causes the error
toast.add({
severity: 'error',
summary: summary,
detail: detail,
life: 3000
})
}
return { addResponseInterceptors }
}
I use this composition function in my main.ts file.
import { useAxios } from '#/services/useAxios'
const { configureAxios } = useAxios()
configureAxios()
use composables are primarily intended to be used directly inside setup function. It's possible to use some of them outside, but this is decided on per case basis depending on composable internals.
The error means that this one appears to use provide/inject and so intended to be used strictly inside component hierarchy, and it's incorrect to use it any where else. Since toasts are displayed by Toast component, the application should be instantiated any way in order to do this.
In this case Axios interceptor could be set inside a component as soon as possible, i.e. inside setup function of root component.
I am working on my first Vue project. I'm used to React and vanilla js, but just getting my head around a few concepts in Vue here.
In particular, importing state and action props from a Pinia store, and seemingly having to import those multiple times in a single Vue component (something I don't need to do in React).
In this example, I am importing a simple count value, and an increment function, and trying to use these in a few different places:
<script setup>
// I import everything initially in setup, which works fine,
// and these props (currentCount and incrementCount)
// can be used in my template:
import { storeToRefs } from 'pinia';
import { useStore } from '#/stores/store';
const { currentCount } = storeToRefs(useStore());
const { incrementCount } = useStore();
</script>
<template>
<main>
Current count: {{ currentCount }}
<button #click="incrementCount">Increment</button>
</main>
</template>
<script>
// I can't use store values from setup here.
// This doesn't work:
// console.log(currentCount);
// I also can't import store values here.
// I get the following error:
// "getActivePinia was called with no active Pinia"
// const { currentCount } = storeToRefs(useStore());
export default {
mounted() {
// I have to import store values here for them to work:
const { currentCount } = storeToRefs(useStore());
console.log(currentCount);
},
watch: {
// weirdly, this reference to watching "currentCount" works:
currentCount() {
// I also have to import store values here for them to work:
const { currentCount } = storeToRefs(useStore());
console.log(currentCount);
},
},
};
</script>
As you can see, if I want to use store values in my template, on mount, and in a watcher (whereby I'd use React's useEffect hook) I am having to import the store props 3 times in total.
Is this correct / normal? Is there a simpler way to achieve what I'm doing, where I only import props once? I want to be sure I haven't missed something and am not doing something in an unusual way.
Thanks for any help and advice!
Pinia was designed with Composition API in mind.
So its intended usage is inside setup() function, where you'd only import it once.
To use it outside of a setup() function, you have two main routes:
inside components, you can just return it from setup() and it becomes available in any hook/method/getter. Either as this.store or spread:
import { useStore } from '#/store'
import { toRefs } from 'vue'
// or from '#vue/composition-api' in Vue2
export default {
setup: () => ({ ...toRefs(useStore()) })
}
/* this makes every state prop, getter or action directly available
on current component instance. In your case, `this.currentCount`.
Obviously, you can also make the entire store available as `this.someStore`:
setup: () => ({ someStore: useSomeStore() })
// now you can use `this.someStore` anywhere
*/
a more general approach is to export the pinia instance (returned by createPinia()), from main.(js|ts), import it where you need the store and then call useStore() passing the pinia instance as an argument.
This can be done anywhere, even outside of components.
Generic example:
import { pinia } from 'main.js'
import { useSomeStore } from '#/store'
const someStore = useSomeStore(pinia);
I should probably also mention the mapState helper provided by pinia. It allows you to select only a few of the keys exposed to current instance. Example:
import { mapState } from 'pinia'
// ...
computed: {
...mapState(useSomeStore, [ 'currentCount'])
}
// Now `this.currentCount` is available
Note: mapState is weirdly named, as it allows you to access more than just state props (also getters and actions). It was named mapState to match the similar helper from vuex.
An even more general approach is to add your store as global, using the plugin registration API in Vue2:
import { useSomeStore } from '#/store';
import { createPinia } from 'pinia';
const pinia = createPinia();
const someStorePlugin = {
install(Vue, options) {
Vue.prototype.someStore = useSomeStore(options.pinia)
}
};
Vue.use(someStorePlugin, { pinia });
new Vue({ pinia });
After this, every single component of your Vue instance will have this.someStore available on it, without you needing to import it.
Note: I haven't tested adding a store in globals (and I definitely advise against it - you should avoid globals), but i expect it to work.
If you want to combine pinia stores with the options API, one way to do it is to use the setup() function inside the options to call useStore:
<script>
import { useStore } from '#/stores/store';
export default {
setup() {
const store = useStore();
return {store}
},
watch: {
store.currentBrightness(newVal, oldVal){
// your code
}
},
methods: {
// inside methods use this.store
},
mounted() {
console.log(this.store.currentCount);
}
}
</script>
Some might consider this as a unwanted mix of composition and options API, but in my view it is a quite good solution for pinia stores.
Nechoj, has the most straightforward answer. Also if you have multiple stores you can always import the stores as necessary into a parent component then use inject just add some parts. For example I have a route data that is called via an api, I don't need it everywhere all the time so i call it in a parent then use inject to use those routes in a drop down that might be a great grandchild component. I don't need that whole utils store just the routes.
index page:
import { useUtilsStore } from "src/stores/utilsStore";
const passengerRoutes = computed(() => utilsStore.getPassengerRoutes);
provide("passengerRoutes", passengerRoutes);
grandchild component:
const compRoutes = inject("passengerRoutes");
I have a situation to convert several jQuery components to VueJs.
In general, I know what to do, but in some cases, I need to replace some functions calls.
For instance:
Component
const Component = (function () {
const initialize = () => {
return 'Tony Stark'
}
return {
initialize: initialize
}
})
export default Component
Random file, using exported function
$( document ).ready(function() {
Component.initialize()
});
What is the best solution to Component.initialize() still working?
Because I have this request in several files.
I got a solution:
import Component from './component'
// Call method
Component.methods.method()
You may import the component to every Vue component and use it like this:
import someComponent from './someComponent'
export default {
created () {
Component.initialize()
}
}
Or you could use instance properties, see https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html
I have a Mixins that get's the data from the sessionStorage, and that data is being used in all component and API call to get data is in the Main component(App.vue) that fetch the data and set's into the sessionStorage.
beforeCreate() {
if (!sessionStorage.getItem('constants')) {
axios.get('/data').then(function(response) {
sessionStorage.setItem('constants',JSON.stringify(response.data.result));
});
},
In the Mixins I'm not getting data from the sessionStorage, Because Mixins runs before the App.vue Component and that does not work.
I also tried to keep the fetch call inside the Mixins but, fetch call is going multiple times even though, I have the condition while getting data from sessionStorag.
import Vue from 'vue';
const Constants = Vue.mixin({
data() {
const constant = JSON.parse(sessionStorage.getItem('constants'));
return {
get constantsData() {
return {
COUNTRY: constant.COUNTRY,
STATE: constant.STATE,
};
},
};
},
});
export default Constants;
What is the best way to utilize Mixins with the API data. ?
You can do
beforeCreate() {
if (!sessionStorage.getItem('constants')) {
axios.get('/data').then(function(response) {
sessionStorage.setItem('constants',JSON.stringify(response.data.result));
this.constant = response.data.result;
});
}
And in mixins:
import Vue from 'vue';
const Constants = Vue.mixin({
data() {
return {
constant: {}
}
},
});
export default Constants;
But normally I prefer using vuex to share data between component.
I found the answer, I used method in the mixins. because I'm using constant as a dropdown value (ie. country name, state from the API). I want to call method only when the dropdown component is created
const Constants = Vue.mixin({
methods: {
getConstantData() {
return JSON.parse(sessionStorage.getItem('constants'));
},
},
data() {
return {};
},
});
export default Constants;
and I can use getConstantData method in any component to access the data just getConstantData()
Let's say I have a component (single file component):
// Cat.vue
export default {
data() {
return {
actionsPredefined: {
eat () => (console.log('eat')),
purr () => (console.log('purr')),
meow () => (console.log('meow')),
}
}
}
}
which is extended by:
// Lion.vue
export default {
extends: Cat,
data() {
return {
actions: {
eat: this.actionsPredefined.eat, // is undefined - why?
purr: this.actionsPredefined.purr, // is undefined - why?
roar () => (console.log('roar'))
}
}
}
}
Now when I use Lion.vue I get:
[Vue warn]: Error in data(): "TypeError: Cannot read property 'eat' of
undefined"
So looks like this.actionsPredefined is undefined in Lion.vue.
What is the correct way here to selectively merge actionsPredefined of extended component (Cat.vue) with actions of extending component (Lion.vue)?
In Lion.vue, when I move actions from data to computed Vue knows what this.actionsPredefined is, but I cannot move it to computed (because I need to be able to change actions by other methods and changing computed values...is obviously against the idea of using computed values and would not work).
I can also set actions empty in Lion.vue data and only populate them with predefinedActions in created() hook and it will work, but somehow it doesn't feel like the right way to do it.
Vue invokes the data function of the subcomponent (Lion) before it calls the data function of the supercomponent (Cat). Data properties defined in Cat won't be available on the instance at that time.
I can also set empty actions in Lion.vue data and populate them only in created() hook and it will work, but somehow it doesn't feel like the right way to do it.
Why not? What difference does it make if you initialize actions in the data hook verses the created hook?
I have made my own tests, and here is what I have found
Extends works fine if you do not create data object on the child class, i.e:
//Lion
<script>
import Cat from './cat';
export default {
extends: Cat,
created() {
this.actionsPredefined.purr(); //this prints purr
}
}
</script>
It also works fine if you define the data and their inherited properties as arrow functions, i.e:
<script>
import Cat from './cat';
export default {
extends: Cat,
data() {
return {
actions: {
eat: () => this.actionsPredefined.eat(),
purr: () => this.actionsPredefined.purr(),
roar: (console.log('roar'))
}
}
},
created() {
this.actions.eat(); // eat
this.actions.purr(); // purr
this.actions.roar; // roar
}
}
</script>
And the Cat component:
<script>
export default {
data() {
return {
actionsPredefined: {
eat: () => (console.log('eat')),
purr: () => (console.log('purr')),
meow: () => (console.log('meow')),
}
}
}
}
</script>