Original question:
vuex shared state in chrome extension
I have the following setup in a chrome extension;
A content script that needs to write to a vuex store
A background script that initializes that store
And a popup script that renders stuff from the store (received from the content script)
store.js
import Vue from "vue";
import Vuex from "vuex";
import "es6-promise/auto";
import createMutationsSharer from "vuex-shared-mutations";
import dummyData from "./dummyData";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
chromePagesState: {
allSections: [],
},
},
mutations: {
setChromePagesState(state, value) {
state.chromePagesState = value;
},
addWhiteListedItem(state, item) {
state.chromePagesState.allSections[0].itemSectionCategory[0].tasks.splice(
0,
0,
item
);
},
},
actions: {
// init from local storage
async loadChromePagesState({ commit }) {
const json = await getStorageValue("inventoryData");
commit(
"setChromePagesState",
Object.keys(json).length === 0 && json.constructor === Object
? dummyData
: JSON.parse(JSON.stringify(json))
);
},
// send message to background script to call init (shortened)
async loadChromePagesStateBrowser({ commit }) {
browser.runtime
.sendMessage({ type: "storeinit", key: "chromePagesState" })
.then(async (chromePagesState) => {
const json = await getStorageValue("inventoryData");
commit(
"setChromePagesState",
Object.keys(json).length === 0 && json.constructor === Object
? dummyData
: JSON.parse(JSON.stringify(json))
);
});
},
},
// stuff from vuex-shared-mutations
plugins: [
createMutationsSharer({
predicate: [
"addWhiteListedItem",
"loadChromePagesState",
"loadChromePagesStateBrowser",
],
}),
],
});
The content script calls store from a vue component:
index.js
import store from "../popup/firstpage/store";
new Vue({
el: overlayContainer,
store,
render: (h) => h(Overlay, { props: { isPopUp: isPopUp } }),
});
Overlay.vue
<script>
import { mapState, mapMutations } from "vuex";
export default {
props: ["isPopUp"],
data() {
return {
};
},
computed: mapState(["chromePagesState"]),
methods: {
...mapMutations(["addWhiteListedItem"]),
// this gets called in the template
addToWhiteList() {
let newItem = initNewItemWithWebPageData();
this.addWhiteListedItem(newItem);
},
},
}
</script>
The background script receives a message and calls a mutation on the store:
background.js
import store from "../content/popup/firstpage/store";
browser.runtime.onMessage.addListener((message, sender) => {
if (message.type === "storeinit") {
store.dispatch("loadChromePagesState");
return Promise.resolve(store.state[message.key]);
}
});
Upon opening popup.js, a store mutation is called that sends a message to background.js that calls another mutation in the store:
popup.js
import store from "./firstpage/store";
export function showPopup() {
const popupContainer = document.createElement("div");
new Vue({
el: popupContainer,
store,
render: (h) => h(App),
created() {
console.log("Calling store dispatch from popup");
this.$store.dispatch("loadChromePagesStateBrowser");
},
});
}
Where App.vue is
<template>
<div id="app">
<OtherComponent />
</div>
</template>
<script>
import { mapActions } from "vuex";
import OtherComponent from "./components/ChromePage.vue";
export default {
name: "App",
OtherComponent: {
VueTabsChrome,
},
methods: {
...mapActions(["loadChromePagesState"]),
},
mounted() {
// once fully mounted we load data
// this is important for a watcher in ChromePage component
console.log("App.vue mounted");
// this.loadChromePagesState();
},
};
</script>
Intuitively export default new creates a new instance on every import hence the not being in sync across scripts (since the stores are different objects).
How can the same store be initialized once and used across multiple entry points?
popup.js is opened when the user clicks the extension icon:
(in this case clicks "new tab").
Related
Can anyone help with the below, I am getting the following error Cannot read properties of undefined (reading 'getters')
I am working on a project where my stores should return an array to my index.vue
Is there also any way I can get around this without having to use the Vuex store?
My store directory contains the below files
index.js
export const state = () => ({})
parkingPlaces.js
import {getters} from '../plugins/base'
const state = () => ({
all: []
});
export default {
state,
mutations: {
SET_PARKINGPLACES(state, parkingPlaces) {
state.all = parkingPlaces
}
},
actions: {
async ENSURE({commit}) {
commit('SET_PARKINGPLACES', [
{
"id": 1,
"name": "Chandler Larson",
"post": "37757",
"coordinates": {
"lng": -1.824377,
"lat": 52.488583
},
"total_spots": 0,
"free_spots": 0
},
]
)
}
},
getters: {
...getters
}
}
index.vue
<template>
<div class="min-h-screen relative max-6/6" >
<GMap class="absolute inset-0 h-100% bg-blue-400"
ref="gMap"
language="en"
:cluster="{options: {styles: clusterStyle}}"
:center="{lat:parkingPlaces[0].coordinates.lat, lng: parkingPlaces[0].coordinates.lng}"
:options="{fullscreenControl: false, styles: mapStyle}"
:zoom="5"
>
<GMapMarker
v-for="location in parkingPlaces"
:key="location.id"
:position="{lat: location.coordinates.lat, lng: location.coordinates.lng}"
:options="{icon: location.free_spots > 0 ? pins.spacefree : pins.spacenotfree}"
#click="currentLocation = location"
>
<GMapInfoWindow :options="{maxWidth: 200}">
<code>
lat: {{ location.coordinates.lat }},
lng: {{ location.coordinates.lng }}
</code>
</GMapInfoWindow>
</GMapMarker>
<GMapCircle :options="circleOptions"/>
</GMap>
</div>
</template>
<script>
import {mapGetters, mapActions} from 'vuex';
export default {
// async mounted() {
// // // console.log('http://localhost:8000/api/parkingPlace')
// // console.log(process.env.API_URL)
// // const response = await this.$axios.$get('PARKING_PLACE')
// //
// // console.log('response', response)
//
// // console.log(location)
// },
data() {
return {
currentLocation: {},
circleOptions: {},
// parkingPlaces: [
//array of parkingPlaces
// ],
pins: {
spacefree: "/parkingicongreen3.png",
spacenotfree: "/parkingiconred3.png",
},
mapStyle: [],
clusterStyle: [
{
url: "https://developers.google.com/maps/documentation/javascript/examples/markerclusterer/m1.png",
width: 56,
height: 56,
textColor: "#fff"
}
]
}
},
computed: {
...mapGetters({
'parkingPlaces': "parkingPlaces/all"
})
},
async fetch() {
await this.ensureParking()
},
methods: {
...mapActions({
ensureParking: 'parkingPlaces/ENSURE'
})
}
}
</script>
base.js
import getters from "./getters";
export {getters};
getters.js
export default {
all: state => state.all
};
Image of my file directory below
image of error
why you need state management :
Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.
today you can try nuxt3 then you have access to Nuxt3 state management
or try Pinia not Vuex
these are better options for Vue3.
if you want to use Vue2, Nuxt2 and Vuex as state management so:
first why your getters file is in plugins?
use this structure:
|__store
⠀⠀|__ index.js
⠀⠀|__ getters.js
your getter file content should be like this :
export default {
//your getters
};
then you can import these getters in your index.js file like this:
import getters from "./getters";
const store = createStore({
state () {
return {
something: 0
}
},
getters
})
and then you use mapGetters:
...mapGetters({
parkingPlaces: 'all'
})
if you have another store you should use modules :
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
you can separate files and the structure will be:
|__ store
⠀⠀|__ index.js # where we assemble modules and export the store
⠀⠀|__ actions.js # root actions
⠀⠀|__mutations.js # root mutations
⠀⠀|__ modules
⠀⠀⠀⠀|__ moduleA.js # moduleA module
⠀⠀⠀⠀|__ moduleB.js # moduleB module
In parkingPlaces.js: Try using import {getters} from '../plugins/base.js' instead of import {getters} from '../plugins/base'.
In base.us: try using import getters from './getters.js' instead of import getters from './getters'.
I believe that you have namespace issue or your store isn't initialized in the right way.
Make sure that you register your parkingPlaces.js module with namespaced property -
import parkingPlacesModule from './parkingPlaces.js';
const store = new Vuex.Store({
modules: {
parkingPlaces: {
namespaced: true,
...parkingPlacesModule
}
}
});
In addition, you can pass the name to the mapGetters helper, like that:
computed: {
...mapGetters('parkingPlaces', [
'all', // -> this.all
])
}
You can try it by below means
In getters.js
export const getters = {
all: state => state.all
};
In base.js
export * from "./getters";
This above way will make your file base.js an object supplying the getters object.
I define tabs as prop, then I want to use in function inside setup() as tabs.value..., but it does not recognize property. It is throwing error:
Cannot find name 'tabs', tabs is not defined
Code:
<script lang="ts">
import {
defineComponent,
ref,
computed,
PropType,
toRefs,
} from '#vue/composition-api'
import i18n from '#/setup/i18n'
export default defineComponent({
name: 'ProgramModal',
props: {
tabs: Array as PropType<Array<any>>,
},
setup() {
const changeTab = (selectedTab: { id: number }) => {
tabs.value.map((t) => {
t.id === selectedTab.id ? (t.current = true) : (t.current = false)
})
}
return {
tabs,
changeTab,
ariaLabel,
}
},
})
</script>
How can I
You need to pass props to setup function:
setup(props) {...
I am new to Typescript with vuex. I simply want to fetch user list from the backend. Put in the store. I declared custom user type
export interface User {
id: number;
firstName: string;
lastName: string;
email: string;
}
in my vuex.d.ts file, I declare store module like:
import { Store } from "vuex";
import { User } from "./customTypes/user";
declare module "#vue/runtime-core" {
interface State {
loading: boolean;
users: Array<User>;
}
interface ComponentCustomProperties {
$store: Store<State>;
}
}
in my store I fetch the users successfully and commit the state:
import { createStore } from "vuex";
import axios from "axios";
import { User, Response } from "./customTypes/user";
export default createStore({
state: {
users: [] as User[], // Type Assertion
loading: false,
},
mutations: {
SET_LOADING(state, status) {
state.loading = status;
},
SET_USERS(state, users) {
state.users = users;
},
},
actions: {
async fetchUsers({ commit }) {
commit("SET_LOADING", true);
const users: Response = await axios.get(
"http://localhost:8000/api/get-friends"
);
commit("SET_LOADING", false);
commit("SET_USERS", users.data);
},
},
getters: {
userList: (state) => {
return state.users;
},
loadingStatus: (state) => {
return state.loading;
},
},
});
I set the getters, I sense that I don't need to set getter for just returning state however this is the only way I could reach the data in my component. Please advise if there is a better way to do it. In my component I accessed the data like:
<div class="friends">
<h1 class="header">Friends</h1>
<loading v-if="loadingStatus" />
<div v-else>
<user-card v-for="user in userList" :user="user" :key="user.id" />
<pagination />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { mapGetters } from "vuex";
import { User } from "../store/customTypes/user";
=import UserCard from "../components/UserCard.vue";
import Loading from "../components/Loading.vue";
import Pagination from "../components/Pagination.vue";
export default defineComponent({
name: "Friends",
components: {
UserCard,
Loading,
Pagination,
},
static: {
visibleUsersPerPageCount: 10,
},
data() {
return {
users: [] as User[],
currentPage: 1,
pageCount: 0,
};
},
computed: {
...mapGetters(["loadingStatus", "userList"]),
},
mounted() {
this.$store.dispatch("fetchUsers");
this.paginate()
},
methods: {
paginate () {
// this.users = this.$store.state.users
console.log(this.$store.state.users)
console.log(this.userList)
}
}
});
</script>
Now when I get userList with getters, I successfully get the data and display in the template. However When I want to use it in the method, I can't access it when component is mounted. I need to paginate it in the methods. So I guess I need to wait until promise is resolved however I couldn't figure out how. I tried
this.$store.dispatch("fetchUsers").then((res) => console.log(res)) didn't work.
What I am doing wrong here?
An action is supposed to return a promise of undefined, it's incorrectly to use it like this.$store.dispatch("fetchUsers").then(res => ...).
The store needs to be accessed after dispatching an action:
this.$store.dispatch("fetchUsers").then(() => {
this.paginate();
});
So I have added a second bus to my code that runs on create, but no matter in which order I call the Busses the second bus (eventBus2) is never called and then returns no data. By printing some console logs I get the feeling that that eventBus2.$on is never executed. Is there some Vue rule that I'm not aware of, any suggestions?
Item.vue
<template>
<div>
<table>
<tr
v-for="item in info"
:key="item.id"
#click="editThisItem(item.id)"
>
<td>{{ item.name}}</td>
<td>{{ item.number}}</td>
<td>{{ item.size}}</td>
</tr>
</table>
</div>
</template>
<script>
import Something from "./Something.vue";
import axios from "axios";
import { eventBus } from "../main";
import { eventBus2 } from "../main";
export default {
components: { Something },
name: "Item",
data() {
return {
selected_item_id: 0,
info: null,
};
},
methods: {
editThisItem(bolt) {
this.selected_item_id = bolt;
eventBus2.$emit("itemWasSelected", this.selected_item_id);
eventBus.$emit("newTabWasAdded", "edit-item");
},
},
mounted() {
axios
.get("http://localhost:8080/items")
.then((response) => (this.info = response.data._embedded.artikli));
},
};
</script>
EditItem.vue
<script>
import Something from "./Something.vue";
import axios from "axios";
import { eventBus2 } from "../main";
export default {
components: { Something},
name: "Edit-item",
data() {
return {
info: null,
select_number: 0,
select_name: "",
selected_item_id: -1,
priv_item: {
id: 0,
size: "big"
},
};
},
mounted() {
if (this.selected_item_id != -1) {
axios
.get("http://localhost:8080/items/" + this.selected_item_id)
.then((response) => (this.priv_item = response.data));
}
},
created() {
eventBus2.$on("itemWasSelected", (data) => {
this.selected_item_id = data;
console.log(" + " + data);
//this console log does not even print the "+", the data is empty
});
console.log(this.selected_item_id);
},
};
</script>
main.js
export const eventBus = new Vue();
export const eventBus2 = new Vue();
you're expecting itemWasSelected and emitting WasSelected they should be the same.
PD: that can be done in one line.
import { eventBus } from "../main";
import { eventBus2 } from "../main";
import { eventBus, eventBus2 } from "../main";
I try to create a custom plugin to store data to use it as global. this is my custom plugin
import {remove} from 'lodash'
export const notifications = {
install(Vue, options = {}) {
Vue.prototype.$notifs = {
count: 0,
notifications: []
}
Vue.prototype.$pushNotifs = ({content, type, timeout}) => {
Vue.prototype.$notifs.count++
Vue.prototype.$notifs.notifications.push({content, type, timeout, id: Vue.prototype.$notifs.count})
}
Vue.prototype.$removeNotifs = ({id}) => {
Vue.prototype.$notifs.notifications = remove(Vue.prototype.$notifs.notifications, (item) => item.id !== id)
}
Vue.mixin({
computed: {
$notifications() {
return this.$notifs.notifications
}
}
})
}
}
when i try to run $pushNotifs methods from my vue template to push some data to $notif.notifications, the template won't updated (but the value its there)
...
methods: {
pushNotifs() {
this.$pushNotifs({content: 'contoh content', type: 'success', timeout: 500})
console.log(this.$notifs.notifications); // has the value
}
}
....
how to make it reactive to the template?
I followed this answer.
Basically, you create a class and use a new Vue instance to provide reactivity.
plugin.js:
import Vue from 'vue';
class Notif {
constructor() {
this.VM = new Vue({
data: () => ({
notifs: [],
count: 0,
}),
});
}
get state() {
return this.VM.$data;
}
get count() {
return this.VM.$data.count;
}
}
const notif = {
Store: Notif,
install (Vue, options) {
Vue.mixin({
beforeCreate() {
this.$notif = options.store;
}
});
},
};
export default waiter;
then to use it (in main.js):
import notif from './plugins/plugin.js';
Vue.use(notif, {
store: new notif.Store()
});
and access it:
this.$notif.state.notifs.push('some notif');
in the template:
<span>{{$notif.count}}</span>
so here state gives you access to all the data, or you can expose individual items as i've shown here.