Vue / Vuex. Vuex store properties not accessible from my component - javascript

I have a vue app, and a component. The component is registered and rendering just fine. I just imported vuex to help with state management. I am using typescript and vue-class-decorator for better type safety.
My app look like so: .ts
// load vuex stores
let store = require('./store/store.ts');
// register components here...
Vue.component('my-component', require('./components/MyComponent.vue'));
// initialize vue applications here...
const app = new Vue({
el: '#app',
store
});
I can console.log the store and see that store is indeed required correctly.
Here's my store: .ts
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
active: false,
},
getters: {
isActive: state => state.active
}
});
Here's my component: .ts
import { Vue, Component, Prop } from 'vue-property-decorator'
import axios from 'axios'
#Component
export default class MyComponent extends Vue {
#Prop(String) route?: string
resent: boolean = false
loading: boolean = false
// constructor
mounted (): void {
if (!this.route) {
throw new Error("The route property is missing");
}
}
get myActiveState (this :Vue): any {
console.log(this.$store);
return this.$store.getters.isActive;
}
}
It doesn't matter what I try, I cannot access the stores state property. I can console log this.$store and I do indeed get the correct store, same if I put a breakpoint in and inspect I can directly access the active property, but if I try and console log this.store.state.active or this.store.getters.isActive, then I get an error.
[Vue warn]: Error in render: "TypeError: Cannot read property 'isActive' of undefined"
I can put a breakpoint in and inspect the console to double check the contents of each property. Everything looks good. BUT i cannot access the property with $store.state.active I have to do $store.default.state.active.
What is going on here? Why can I not access the state when it's coded? Additionally trying to code this.$store.default.state.active gives me a build error property does not exist on type Store<any>

This guy applies a nice solution to this problem https://www.youtube.com/watch?v=V9MmoBAezD8
in a nutshell, you have to create a shims-vuex.d.ts to solve this problem, as documented by vuex v4.0.0 release
the State type will be exported from your store/index.ts and it should be a representation of your state
import { State } from './store/index.ts';
declare module '#vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<State>;
}
}
// Vuex#4.0.0-beta.1 is missing the typing for `useStore`. See https://github.com/vuejs/vuex/issues/1736
declare module 'vuex' {
export function useStore(key?: string): Store<State>;
}

Related

Is there a way to encapsulate Vuex store inside Vue plugin (its install function)?

I have a plugin and this plugin uses Vuex
// plugin.js
import Vuex from "vuex";
import store from "./store.js";
export default {
install(Vue, options) {
const storeInstance = new Vuex.Store(store);
Vue.prototype.$store = storeInstance;
}
};
And in that plugin I import a store object.
// store.js
export default {
actions: {
SOME_RANDOM_ACTION({ state, commit }) {
console.log("some random action");
}
}
};
Dispatching actions and using state is fine and works as expected.
But when I add this plugin to another Vue instance that uses vuex, store object re-initializes with new state.
// index.js
import Vue from "vue";
import Vuex from "vuex";
import App from "./App.vue";
import plugin from "./plugin.js";
Vue.use(Vuex);
Vue.use(plugin);
new Vue({
// WARN when i uncomment this next line of code Vuex gets re-initialized with new object
// store: new Vuex.Store({ state: { hello: "hix" } }),
components: {
App
}
}).$mount("#app");
When you uncomment store initialization, store that was defined in the plugin is now not available.
Currently, I have these solutions in mind:
Export my plugin store object to index.js main app, and use this store as a module.
Use some other state management.
Is there a way to use Vuex inside my plugin?
https://codesandbox.io/s/vibrant-sanne-67yej?file=/src/main.js:0-371
Vuex plugin uses store option to assign store instance to Vue.prototype.$store, similarly to your own plugin.
If the intention is to use multiple stores, their names shouldn't collide. The key is to name your store object inside plugin something other than $store
Vue.prototype.$myPluginStore = storeInstance;
But this still doesn't encapsulate $myPluginStore inside the plugin, as it is accessible within the app.
// App.vue
computed: {
appState() {
return this.$store.state;
},
pluginState() {
return this.$myPluginStore.state; // this is now accessible within the main app
}
}
It would be a reasonable solution to allow a store to be used as a module of existing store instead of creating a new store, but only when used within one app and not when used as a plugin for a package.
The main problem is that default store instance ($store) can make use of Vuex helpers - mapGetters, etc.
You can take advantage of the install method exposed by the plugin to get access to the store - which should be accessible from your other component.
One possible solution is to register your store in the index.js like:
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
import plugin from "./plugin";
Vue.use(plugin);
new Vue({
store,
components: {
App
}
}).$mount("#app");
You can then expose $doStuff() and get access to $store in the plugin.js
export default {
install(Vue) {
Vue.prototype.$doStuff = function (payload) {
this.$store.dispatch("SOME_RANDOM_ACTION", payload);
};
}
};
The store instance is accessible from your plugin or all the other components.
You can see a working sample here

Set a class to prototype in NuxtJS Vue

How to set a class to prototype correctly in Vue NuxtJS?
I create plugin
nuxt.config.js
plugins: [
{ src: "~/plugins/global.js" },
],
global.js
import Vue from "vue";
import CustomStore from "devextreme/data/custom_store";
//try set in prototype
Vue.use(CustomStore)
have error
A class must be instantiated using the 'new'
I understand that this is not correct, but I can not find anywhere how to initialize it
Vue.use(new CustomStore());
no error but how call?
I want to use something like this in my component
this.dataSource = this.$CustomStore({ ///... settings...// })
I assume that CustomStore is a function, so you can try using Nuxt.js inject() method. This method will make your functions or values available across your app.
~/plugins/global.js
export default ({ app }, inject) => {
// Inject $CustomStore() in Vue, context and store.
inject('CustomStore', CustomStore)
}
then you can use it across your app components.
your component
mounted() {
this.$CustomStore()
},
reference https://nuxtjs.org/docs/2.x/directory-structure/plugins#inject-in-root--context

Conditional import in Vue component

I have a root instance that has several CustomVideo-components in it (amongst a bunch of other components). The CustomVideo-component implements VideoJS, but it's not on all pages that there is a CustomVideo-component, so I don't want to import VideoJS globally. Here is an example of components on a page:
App.js
|
|-- CustomVideo
|-- FooComponent
|-- CustomVideo
|-- BarComponent
|-- CustomVideo
In the top of CustomVideo, I import VideoJS, like so:
import videojs from 'video.js';
import abLoopPlugin from 'videojs-abloop'
export default {
name: "FeaturedVideoPlayer",
props: {
videoUrl: String
}
mounted() {
let videoOptions = {
sources: [
{
src: this.videoUrl,
type: "video/mp4"
}
],
plugins: {
abLoopPlugin: {
'enabled': true
}
}
};
this.player = videojs(this.$refs.featuredVideoPlayer, videoOptions, function onPlayerReady() {});
}
But if there are more than one CustomVideo, then I get a console warning:
VIDEOJS: WARN: A plugin named "abLoopPlugin" already exists. You may want to avoid re-registering plugins!
I tried looking into conditional imports, but it doesn't seem like it's the way to do it.
Even if I try and import it in app.js, even though I would rather import it CustomVideo, then I get another console error:
Attempt
import abLoopPlugin from 'videojs-abloop'
Vue.use( abLoopPlugin );
Then I get the error:
Uncaught TypeError: Cannot read property 'registerPlugin' of undefined
How do I ensure that a plugin is registered only once?
Check videojs.getPlugins().abLoopPlugin
videojs.getPlugins() returns a symbol table of all loaded plugin names. You could simply check that abLoopPlugin is not in that table before loading it in your component:
import videojs from 'video.js'
if (!videojs.getPlugins().abLoopPlugin) {
abLoopPlugin(window, videojs)
}
Await $nextTick before using ref
You'll notice that your videos are initially not visible in your specified <video> element. This is because the ref is undefined when you pass it to videojs in mounted().
The ref docs state:
An important note about the ref registration timing: because the refs themselves are created as a result of the render function, you cannot access them on the initial render - they don’t exist yet!
The solution is to wait until the $nextTick():
async mounted() {
// this.$refs.featuredVideoPlayer is undefined here
await this.$nextTick()
// this.$refs.featuredVideoPlayer is the <video> here
this.player = videojs(this.$refs.featuredVideoPlayer)
}
Try creating instance of abLoopPlugin. I followed the same approach for vue-i18n and other plugins to use globally. Something like:
import AbLoopPlugin from 'videojs-abloop'
Vue.use(AbLoopPlugin) // CHECK IF REGISTER PLUGIN ERROR PERSIST
const abLoopPlugin = new AbLoopPlugin({
---your settings---
})
export default abLoopPlugin

How to register custom Vue components?

I promise I've read every related post I can find, but I'm getting any clear answers (as far as I can tell, but I'm new to Vue).
Details:
I'm using Vue v2.5.15
We are using webpack (I've read that this may affect how I have to use components)
I'm attempting to use this range slider component. I've tried to follow the documentation, but I keep getting an error stating [Vue warn]: Unknown custom element: <vue-slider> - did you register the component correctly?. I've tried reading the documentation and probably 20 different posts, but I'm still not getting this. Here is basically what I have in my JS file:
<!-- HTML -->
<div id="app">
<vue-slider
v-model="sliderRange"
:min="minPriceBase"
:max="maxPriceBase"
></vue-slider>
</div>
// Javascript
import Vue from 'vue';
import VueSlider from 'vue-slider-component';
import 'vue-slider-component/theme/default.css';
export default {
components: {
VueSlider
}
}
Vue.component('VueSlider', VueSlider);
var vm = new Vue({
el: '#app',
data: {},
components: {
VueSlider: window['vue-slider-component']
}
});
I've also read through the Vue docs on components and I can't figure what I'm doing wrong. How do I correctly register my component?
Update:
I've discovered that if I use:
components: {
VueSlider
}
Instead of:
components: {
VueSlider: window['vue-slider-component']
}
Then I stop getting error mentioned above. Instead I get an error stating [Vue warn]: Invalid prop: type check failed for prop "min". Expected Number, got Boolean. But think it's progress? Why would `window['vue-slider-component'] be necessary?
Looking at the docs, i think you tried to do all the different import variants at the same time
new Vue({
el: '#app',
components: {
VueSlider: window['vue-slider-component']
}
})
seems to be the suggested way of accessing the component that was appended to the window if you do a direct import via the script by adding
<script src="./node_modules/vue-slider-component/dist/vue-slider-component.umd.min.js"></script>
(which you aren't doing)
Then there is two more ways, you can install the component globally by using
// main.js
import VueSlider from 'vue-slider-component'
import 'vue-slider-component/theme/default.css'
Vue.component('VueSlider', VueSlider)
which would then make the vue-slider available to all the components in your application, so you don't have to import it multiple times. If you do this, you won't have to also define it locally. And finally the third option is to import it only locally:
// App.vue
import VueSlider from 'vue-slider-component'
import 'vue-slider-component/theme/default.css'
export default {
components: {
VueSlider
}
}
This would make the slider component only available to a single component. In your case, you can decide to either go for option 1:
import VueSlider from 'vue-slider-component'
Vue.component('VueSlider', VueSlider);
or option 2:
import VueSlider from 'vue-slider-component'
import 'vue-slider-component/theme/default.css'
export default {
components: {
VueSlider
}
}
The error you are seeing now by the way, is that your minPriceBase that you're referencing is not a Number, you would have to set that data on the instance:
import VueSlider from 'vue-slider-component'
import 'vue-slider-component/theme/default.css'
export default {
components: {
VueSlider
},
data () {
return {
minPriceBase: 0,
maxPricebase: 10
}
}
}
Be aware that in your code you pasted the export before your vue instance, you don't need to export anything there, you just need to define that component on the vue instance:
import VueSlider from 'vue-slider-component'
var vm = new Vue({
el: '#app',
data: {},
components: {
VueSlider
}
});
you should be able to get rid of this in your example, it doesn't do anything:
export default {
components: {
VueSlider
}
}
Recently, there was same problem and there was also 500 server side rendering error.
Anyway, I fixed and used that by using require.
if (process.browser) { // I am not sure if it is needed for you.
const VueSlider = require('vue-slider-component');
Vue.component('VueSlider', VueSlider);
Vue.use(require('vue-slider-component/theme/default.css'));
}
...
<vue-slider ...>
...
</vue-slider>

declare mapState and mapMutations globally in SPA VueJS

I am creating a basic SPA, but it happens that the states I manage with Vuex and the mutations up there all right, but in each component that I want to use mapState and mapMutations I have to import them locally.
<script>
import {mapState,mapMutations } from 'vuex';
export default{
computed : mapState(['isLoggedIn']),
methods: {
...mapMutations(['logout'])
}
}
</script>
This is the correct way to do it? Or how can I declare them globally and avoid importing in each component so that it is as follows?
<script>
export default{
computed : mapState(['isLoggedIn']),
methods: {
...mapMutations(['logout'])
}
}
</script>
Quick solutions
The Vuex helpers like mapMutations etc. returns an object populated with functions which assumes that this.$store is a Vuex store.
Store service
If you need to use the store in vanilla JS and you don't want to expose the store module everywhere, you could define a service module.
import { mapActions } from 'vuex';
import $store from '#/store';
/**
* Simple mapping of the Vuex store UI module.
* #module services/ui
* #property {Function} pushMessage
*/
export default Object.assign({
$store,
}, mapActions('ui', ['pushMessage']));
Now, using it is as simple as importing the service module.
import ui from './services/ui';
// triggers the `pushAction` on the ui namespaced store module
ui.pushMessage({ content: 'whatever' });
Vue mixin
To have default computed and methods on a Vue component, you can create a simple mixin to import in specific components.
import { mapState, mapMutations } from 'vuex';
export default {
computed : mapState(['isLoggedIn']),
methods: mapMutations(['logout'])
}
Then, use the mixin in a component.
<script>
import mixin from './mixin';
export default {
mixins: [mixin],
data() {/* ... */},
// etc.
}
</script>
Global mixin
If you really want each component to have this default mixin without having to explicitly define it, use a global mixin.
import Vue from 'vue';
import { mapState, mapMutations } from 'vuex';
export const mixin = {
computed : mapState(['isLoggedIn']),
methods: mapMutations(['logout'])
};
Vue.mixin(mixin);
Personally, as a standard, I never map the state or mutations.
I only map getters and actions so my components don't need to know the state structure and that an action is async or not. I see getters and actions as the public API of a Vuex store (or any similar store, like Redux).
I also only map the relevant data. Components responsibility is to interact with the user of the app, displaying and handling events and nothing more. If all your components need the user object, maybe they're doing too much and that logic should probably be moved elsewhere, like in an action.
See more on Vue communication

Categories

Resources