VueJS: Best practice for working with global object between components? - javascript

there is User.js class and user object(user = new User();).
The user object is being used in all nested components. in User class there are so many important methods.
How can I simply use/access this.user or this.$user and its methods in any component?
1-solution (temporary working solution): Setting user in vuex's store and define in all components' data:
data(){
return {
user:this.$store.state.user
}
}
Cons: in every component, this should be added. Note: there are so many components.
2-solution: adding user to Vue's prototype like plugin:
Vue.prototype.$user = user
Cons: when user's data changes, it doesn't effect in DOM element (UI).
3-solution: putting to components's props.
Cons: in every component, this should be added. Note: Again there are so many components.
All of the solutions I found have issues, especially as the project gets larger and larger.
Any suggestion and solution will be appreciated!

Note: Applies for Vue 2x
Proposal 1: Using getters from vuex
You could use getters along with mapGetters from Vuex to include users within computed properties for each component.
Vuex
getters: {
// ...
getUser: (state, getters) => {
return getters.user
}
}
component
import { mapGetters } from 'vuex'
computed: {
...mapGetters([getUser])
}
Proposal 2: add a watcher via plugin
Vue
// When using CommonJS via Browserify or Webpack
const Vue = require('vue')
const UserPlug = require('./user-watcher-plugin')
// Don't forget to call this
Vue.use(UserPlug)
user-watcher-plugin.js
const UserPlug = {
install(Vue, options) {
// We call Vue.mixin() here to inject functionality into all components.
Vue.watch: 'user'
}
};
export default UserPlug;
Proposal 3: add a computed property user as plugin via mixin
Vue
// When using CommonJS via Browserify or Webpack
const Vue = require('vue')
const UserPlug = require('./user-watcher-plugin')
// Don't forget to call this
Vue.use(UserPlug)
user-watcher-plugin.js
const UserPlug = {
install(Vue, options) {
// We call Vue.mixin() here to inject functionality into all components.
Vue.mixin({
computed: {
user: function() {
return this.$store.state.user
}
}
})
}
};
export default UserPlug;

Based on #Denis answer, specifically Proposal 3, Here is the UserPlugin.js:
import store from '#/store/store';
import User from './User';
const UserPlugin = {
install(Vue) {
const $user = new User();
window.$user = $user;
store.commit('setUser', $user);
Vue.mixin({
computed: {
$user() {
return store.state.user;
}
}
});
}
};
export default UserPlugin;
and main.js:
import UserPlugin from './common/UserPlugin';
Vue.use(UserPlugin);
new Vue({
render: h => h(App)
}).$mount('#app');
For further usage, I published small library for solving these kinda issues:
https://www.npmjs.com/package/vue-global-var

Assuming you don't actually use all methods/attributes of user in every component, but a subset of them everytime, I don't see any reason why solution 1 & 2 do not work for you, since passing the whole user object to every component is not necessary.
Let's say your object User have some attributes (a1, a2, a3, etc.) and methods (m1, m2, m3...). If a component only needs some of them (e.g. a1, a2, m1, m2, m3) then with Vuex, you can use mapping functions (mapState, mapGetters, mapMutations and mapActions) to get the exact info from user
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
...mapState('user', [ 'a1' ]),
...mapGetters('user', [ 'a2' ])
},
methods: {
...mapMutations('user', [ 'm1' ]),
...mapActions('user', [ 'm2', 'm3' ])
}
}
For solution 2 (using prototype), to make component update when user data changes, you can map the necessary data to component via methods.
export default {
methods: {
userA1() {
return this.$user.attributes.a1;
},
userM1() {
this.$user.methods.m1();
}
// and so on
}
}
Even better, you can create mixins to explicitly map data from user, and reuse your mixins to avoid duplicated code in components. It can be applied for both Vuex solution and prototype solution.
// mixin1:
const mixin1 = {
computed: {
...mapState('user', [ 'a1' ]),
},
methods: {
...mapMutations('user', [ 'm1' ])
}
}
// mixin2:
const mixin2 = {
computed: {
...mapGetters('user', [ 'a2' ]),
},
methods: {
...mapActions('user', [ 'm2', 'm3' ])
}
}
// component1
export default {
mixins: [ mixin1 ]
}
// component 2
export default {
mixins: [ mixin1, mixin2 ]
}
But if you really need to pass the whole object user to every component, then nothing could do. Rather, you should review your implementation and see if there is any better way to break the object into smaller meaningful ones.

You can use mixins to add User.js to your root component like
import userLib from './User';
//User.js path should correct
Then
var app = new Vue({
router,
mixins: [
userLib
],
//.....
});
After that you can use any of these User method in your any component like
this.$parent.userClassMehtod();
or if any data access
this.$parent.userClassData;
Finally dont forget to add export default{//..} in User.js
Note: This is only work if you export all method of User.js into export default

I just created the minimal codesandbox to clear the idea of how dependency Injection works in vue.

You can have a second Vue instance and declare a reactive property.
See: Reactivity in depth

Related

How to access nuxt `$config` in Vuex state? Only access method is through store actions methods?

I have used to dotenv library to use .env file, but I have to change runtimeConfig because I realized it was easy to expose my project secret key.
In my latest project, I have used nuxt "^2.14" and mode is SPA.
So I only use "publicRuntimeConfig" in nuxt.config.ts like that.
.env
Test_BASE_URL:'https://test.org'
nuxt.config.ts
export default {
publicRuntimeConfig:{baseURL: proccess.env.Test_BASE_URL||''}
}
I can use env like that in vue file.
sample.vue
<script>
export default {
mounted(){
console.log(this.$config.baseURL)
}
}
</script>
But I couldn't use "$config" in store's state.
I tried to write that but it always return "undefied"
index.ts
export const state = (context) => ({
url:context.$config
})
I have referred the this guys solutions
and changed state's value through the actions method.
I have used SPA, so I made method like 'nuxtServerInit'as plugins.
plugins/clientInit.ts
import {Context} from "#nuxt/types";
export default function (context:Context) {
context.store.dispatch('initEnvURL',context.$config)
}
index.ts
interface State {
testURL: string
}
const state = () => ({
testURL:''
})
const mutations = {
setTestURl(state:State,config:any) {
state.testURL = config.baseURL
}
const actions = {
initEnvURL({commit},$config) {
commit('setTestURl',$config)
}
}
export default {state,mutations,actions}
I success to change state value through actions methods above,
but I don't know why "context" can't use store/state objects directly.
Does anyone know how to use $config in store/state?
or is it impossible only way to use $config through actions method like above?
That's because in Vuex, only actions actually receive the app context.
State, Mutations and Getters can't access it by design.
Your initial state should be contextless, i.e. with values that doesn't depend on the runtime execution.
Mutations are stateless, they just take a parameter and update the state. That's all. Contextful parameters should be coming from the caller.
Getters are just reactive state transformations, and should not rely on context properties, that would be messing with the Vuex module state.
So yes, what you have to do it initialise your store within the nuxtServerInit actions (or from a plugin for SPA apps):
nuxtServerInit({ store, config } ) {
store.commit('UPDATE_BASE_URL', config.baseUrl)
}
It does NOT show up through the type system even when using #nuxt/types.
Access it like this in store/index.ts or store/module.ts:
import { ActionTree, MutationTree } from 'vuex'
const actions: ActionTree<ModuleState, RootState> = {
async yourActionName({ commit }, payload): Promise<void> {
try {
let url = this.app.$config.baseURL + "/path"; // <- config is accessed here.
const res = await this.$axios.get<number>(url);
commit("mutateState", res.data);
return;
} catch (error) {
// Error handling
}
},
};
My nuxt.config.js looks like:
export default {
...
publicRuntimeConfig: {
baseURL: process.env.BASE_URL || 'http://localhost:5000/api',
}
...
};

With Vue-cli, where do I declare my global variables?

In most Vue.js tutorials, I see stuff like
new Vue({
store, // inject store to all children
el: '#app',
render: h => h(App)
})
But I'm using vue-cli (I'm actually using quasar) and it declares the Vue instance for me, so I don't know where I'm supposed to say that I want store to be a "Vue-wide" global variable. Where do I specify that? Thanks
Yea, you can set those variables like this, in your entrypoint file (main.js):
Vue.store= Vue.prototype.store = 'THIS IS STORE VARIABLE';
and later access it in your vue instance like this:
<script>
export default {
name: 'HelloWorld',
methods: {
yourMethod() {
this.store // can be accessible here.
}
}
}
</script>
You can also see this in the vue-docs here.
Edit 1:
from the discussions in the comment sections about "no entrypoint file" in quasar's template.
what you can do is, to go to src/router/index.js, and there you will be able to get access to Vue, through which you can set a global variable like this:
...
import routes from './routes'
Vue.prototype.a = '123';
Vue.use(VueRouter)
...
and then if you console.log it in App.vue, something like this:
<script>
export default {
name: 'App',
mounted() {
console.log(this.a);
}
}
</script>
now, look at your console:
You can also do the same in App.vue file in the script tag.
You don't need to make the store a global variable like that, as every component (this.$store) and the Vue instance itself have access to the store after the initial declaration.
Take a look at the Quasar docs for App Vuex Store.
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
updateCount(state) {
state.count += 1
}
}
})
main.js
import App from './App.vue'
import store from '/path/to/store.js'
new Vue({
el: '#app',
store,
render: h => h(App)
})
If you need to access the store from within a component you can either import it (as we did in main.js) and use it directly [note that this is a bad practice] or access using this.$store. You can read a bit more about that here.
In any case here's the official Getting Started guide from Vuex team
We could add the Instance Properties
Like this, we can define instance properties.
Vue.prototype.$appName = 'My App'
Now $appName is available on all Vue instances, even before creation.
If we run:
new Vue({
beforeCreate: function() {
console.log(this.$appName)
}
})
Then "My App" will be logged to the console!
Slightly redundant to the aforementioned answer, but I found this to be simpler per the current Vuex state documentation at the time of this reply.
index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default function (/* { ssrContext } */) {
const Store = new Vuex.Store({
modules: {
// example
},
state: {
cdn_url: 'https://assets.yourdomain.com/'
},
// for dev mode only
strict: process.env.DEV
})
return Store
}
...and then in your component, e.g. YourPage.vuex
export default {
name: 'YourPage',
loadImages: function () {
img.src = this.$store.state.cdn_url + `yourimage.jpg`
}
}
Joining the show a bit late, but the route I personally use in Quasar is to create a Boot file for my global constants and variables.
I create the Boot file (I call it global-constants.js but feel free to call it whatever).
/src/boot/global-constants.js
import Vue from 'vue'
Vue.prototype.globalConstants = {
baseUrl: {
website: 'https://my.fancy.website.example.com',
api: 'https://my.fancy.website.example.com/API/v1'
}
}
if (process.env.DEV) {
Vue.prototype.globalConstants.baseUrl.website = 'http://localhost'
Vue.prototype.globalConstants.baseUrl.api = 'http://localhost/API/v1'
}
if (process.env.DEV) {
console.log('Global Constants:')
console.log(Vue.prototype.globalConstants)
}
Then add a line in quasar.conf.js file to get your Boot file to kick:
/quasar.conf.js
module.exports = function (ctx) {
return {
boot: [
'i18n',
'axios',
'notify-defaults',
'global-constants' // Global Constants and Variables
],
Then to use it:
from Vuex
this._vm.globalConstants.baseUrl.api
for example: axios.post(this._vm.globalConstants.baseUrl.api + '/UpdateUserPreferences/', payload)
from Vue HTML page
{{ globalConstants.baseUrl.api }}
from Vue code (JavaScript part of Vue page
this.globalConstants.baseUrl.api
An alternative Vue3 way to this answer:
// Vue3
const app = Vue.createApp({})
app.config.globalProperties.$appName = 'My App'
app.component('child-component', {
mounted() {
console.log(this.$appName) // 'My App'
}
})

accessing vuex store in js file

Just like in main.js, I'm trying to access my store from a helper function file:
import store from '../store'
let auth = store.getters.config.urls.auth
But it logs an error:
Uncaught TypeError: Cannot read property 'getters' of undefined.
I have tried
this.$store.getters.config.urls.auth
Same result.
store:
//Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
config: 'config',
},
getters: {
config: state => state.config
},
});
export default store
How do I make my store available outside of components?
The following worked for me:
import store from '../store'
store.getters.config
// => 'config'
This Worked For Me In 2021
I tried a bunch of different things and it seems, at least in Vue 3, that this works. Here is an example store:
export default {
user: {
bearerToken: 'initial',
},
};
Here is my Getters file:
export default {
token: (state) => () => state.user.bearerToken,
};
Inside your .js file add the page to your store\index.js file.
import store from '../store';
In order to access the getters just remember it is a function (which may seem different when you use mapGetters.)
console.log('Checking the getters:', store.getters.token());
The state is more direct:
console.log('Checking the state:', store.state.user.bearerToken);
If you are using namespaced modules, you might encounter the same difficulties I had while trying to retrieve items from the store;
what might work out for you is to specify the namespace while calling the getters (example bellow)
import store from '../your-path-to-your-store-file/store.js'
console.log(store.getters.['module/module_getter']);
// for instance
console.log(store.getters.['auth/data']);
put brackets on your import and it should work
import { store } from '../store'
using this approach has worked for me:
// app.js
import store from "./store/index"
const app = new Vue({
el: '#app',
store, //vuex
});
window.App = app;
// inside your helper method
window.App.$store.commit("commitName" , value);
if you are using nuxt you can use this approach
window.$nuxt.$store.getters.myVar
if you have multiple modules
window.$nuxt.$store.getters['myModule/myVar']
export default ( { store } ) => {
store.getters...
}

Separate vuex stores for component created with v-for

Imagine three components First.vue, Second.vue and Third.vue
They share some info, to manage all changes and get access to data I use Vuex.
It works perfect. No problem here. To keep store clean and tidy, I have modules for every component.
Then, we have a component named Card.vue. It is composed of three components, mentioned above.
In App.vue, I use v-for directive to create as many Cards as needed.
<template>
<div>
<card v-for="card in cards" :id="card"></card
</div>
</template>
<script>
export default {
data () {
return {
cards: [] // items being dynamically added to this array
}
</script>
As you understand, when we generate more than one of Card.vue, we have several cards with the same data. In other words when I change any value in input field of Card#1, I commit change to $store, and update state everywhere else (in Card#2, Card#3 and so on...) What I need is to separate those Cards data. How can I design my Vuex to accomplish that goal.
Sharing STATE and COMPONENTS
$store
import job from './modules/job'
import paper from './modules/paper'
import admin from './modules/admin'
import * as getters from './getters'
import * as mutations from './mutations'
export const store = new Vuex.Store({
getters,
mutations,
modules: {
job,
paper,
admin
}
})
I use three modules and keep my getters and mutations in separate js file. Module files have the same structure. For example, Job.js is like:
const state = {
paper: {
paperWidth: null,
paperHeight: null,
typeOfPaper: null,
...// other state data
}
}
const getters = {
paper: state => state.paper
}
const mutations = {
updatePaper (state, paper) {
Object.assign(state.paper, paper)
},
...// other local mutations go here
export default {
state,
getters,
mutations
}
I've no actions. No need as of now.
Three components are Job.vue, Paper.vue and Rseults.vue. They get state and mutations from their respective module and also from global store.
import {mapMutations, mapGetters} from 'vuex'
export default {
computed: {
...mapGetters([
'job',
'paper',
...// other shared mutations from store
])
},
methods: {
...mapMutations([
'fourPlusFour',
'fourPlusZero',
'updateJob',
...// other shared mutations from store
])
}
}

Pass prop as module name when mapping to namespaced module

I'm trying to pass the store module namespace via props to a component. When I try and map to getters with the prop, it throws this error,
Uncaught TypeError: Cannot convert undefined or null to object
If I pass the name as a string it works.
This Works
<script>
export default {
props: ['store'],
computed: {
...mapGetters('someString', [
'filters'
])
}
}
</script>
This does not work
this.store is defined
this.store typeof is a String
<script>
export default {
props: ['store'],
computed: {
...mapGetters(this.store, [
'filters'
])
}
}
</script>
I used this style utilising beforeCreate to access the variables you want, I used the props passed into the component instance:
import { createNamespacedHelpers } from "vuex";
import module from '#/store/modules/mymod';
export default {
name: "someComponent",
props: ['namespace'],
beforeCreate() {
let namespace = this.$options.propsData.namespace;
const { mapActions, mapState } = createNamespacedHelpers(namespace);
// register your module first
this.$store.registerModule(namespace, module);
// now that createNamespacedHelpers can use props we can now use neater mapping
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
}),
// because we use spread operator above we can still add component specifics
aFunctionComputed(){ return this.name + "functions";},
anArrowComputed: () => `${this.name}arrows`,
};
// set up your method bindings via the $options variable
this.$options.methods = {
...mapActions(["initialiseModuleData"])
};
},
created() {
// call your actions passing your payloads in the first param if you need
this.initialiseModuleData({ id: 123, name: "Tom" });
}
}
I personally use a helper function in the module I'm importing to get a namespace, so if I hadmy module storing projects and passed a projectId of 123 to my component/page using router and/or props it would look like this:
import { createNamespacedHelpers } from "vuex";
import projectModule from '#/store/project.module';
export default{
props['projectId'], // eg. 123
...
beforeCreate() {
// dynamic namespace built using whatever module you want:
let namespace = projectModule.buildNamespace(this.$options.propsData.projectId); // 'project:123'
// ... everything else as above with no need to drop namespaces everywhere
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
})
}
}
}
Hope you find this useful.
I tackled this problem for hours, too. Then I finally came up with one idea.
Add attachStore function in a child vue component. A function nama is not important. Any name is ok except vue reserved word.
export default {
:
attachStore (namespace) {
Object.assign(this.computed, mapGetters(namespace, ['filters']))
}
}
When this vue component is imported, call attachStore with namespace parameter. Then use it at parent components attributes.
import Child from './path/to/child'
Child.attachStore('someStoresName')
export default {
name: 'parent',
components: { Child }
:
}
The error you're encountering is being thrown during Vue/Vuex's initialization process, this.store cannot be converted because it doesn't exist yet. I haven't had to work with namespacing yet, and this is untested so I don't know if it will work, but you may be able to solve this problem by having an intermediary like this:
<script>
export default {
props: ['store'],
data {
namespace: (this.store !== undefined) ? this.store : 'null',
},
computed: {
...mapGetters(this.namespace, [
'filters'
])
}
}
</script>
That ternary expression will return a string if this.store is undefined, if it isn't undefined then it will return the value in this.store.
Note that there is also a discussion about this on Vue's Github page here: https://github.com/vuejs/vuex/issues/863
Until Vue formally supports it, I replaced something like
...mapState({
foo: state => state.foo
})
with
foo () {
return this.$store.state[this.namespace + '/foo'] || 0
}
Where namespace is passed to my child component using a prop:
props: {
namespace: { type: String, required: true }
}

Categories

Resources