Let's say that I have a script file, foo.js:
function doStuff() {
// how to access store and other plugins here?
}
export default { doStuff }
Without passing the calling component as an argument, how can I access things like app or installed plugins like store, i18n in a script file like the one above?
There are multiple ways to call custom function with this being a reference to the component it was invoked in.
1) Using mixins
Custom function can be declared as a method and used within component via this.customMethod.
customHelpers.js
const customHelpers = {
methods: {
doStuff () {
// this will be referenced to component it is executed in
}
}
}
component.vue
// component.vue
import customHelpers from '~/mixins/customHelpers'
export default {
mixins: [customHelpers],
mounted () {
this.doStuff()
}
}
2. Using context injection
Declare custom plugin:
plugins/customHelpers.js
import Vue from 'vue'
Vue.prototype.$doStuff = () => { /* stuff happens here */ }
And use plugin in nuxt.config.js
export default {
..., // other nuxt options
plugins: ['~/plugins/customHelpers.js']
}
This makes method available inside every component:
export default {
mounted () {
this.$doStuff()
}
}
3) Using combined injection
Same as method 2, but injection will be also accessible inside fetch, asyncData and inside store modules. Bindings to this may vary, since context is not available everywhere.
plugins/customHelpers.js
export default ({ app }, inject) => {
inject('doStuff', () => { /* stuff happens here */ })
}
And use plugin in nuxt.config.js
export default {
..., // other nuxt options
plugins: ['~/plugins/customHelpers.js']
}
Usage example:
export default {
asyncData ({ app }) {
app.$doStuff()
}
}
Please, refer to documentation for more examples.
Related
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");
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);
}
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()
I am using Webpack 2 and importing my components via special require syntax.
There are over 100 components, but only 5-10 used at a time. Most of them (but not all) partially have same functionality like props and lifecycle hooks.
Here is code:
// app.js
...
Vue.component("foo", resolve => {
require(['./components/foo.vue'], resolve);
});
...
I want to apply mixin to async component, but how to do that? Global mixin apply to all components, but that's not what I need.
I found that feature request, but it closed.
I found some creepy(?) solution, but it works:
// mixins.js
export default class Mixins {
static fooMixin() {
return {
created: function () {
console.log('mixin hook called');
}
}
}
}
// app.js
Vue.component("foo", resolve => {
require(['./components/foo.vue'], resolve);
});
// foo.vue
<script>
import Mixins from "mixins";
export default {
...
mixins: [Mixins.fooMixin()]
}
</script>
But I hope that there is a more elegant solution.