Vue composition API store - vue.js

I was wondering if it was possible with the new Vue composition API to store all the ref() in one big object and use that instead of a vuex store. It would certainly take away the need for mutations, actions, ... and probably be faster too.
So in short, is it possible to have one place for storing reactive properties that will share the same state between different components?
I know it's possible to have reusable code or functions that can be shared between different components. But they always instantiate a new object I believe. It would be great if they would depend on one single source of truth for a specific object. Maybe I'm mixing things up...

You can use reactive instead of ref.
For eg.
You can use
export const rctStore = reactive({
loading: false,
list: [],
messages: []
})
Instead of
export const loading = ref(false)
export const list = ref([])
export const messages = ref([])
Cheers!

Related

Vue computed() not triggered on reactive map

I have a reactive around a map that's initially empty: const map = reactive({});, and a computed that tells if the map has a key "key": const mapContainsKeyComputed = computed(() => map.hasOwnProperty("key")). The computed doesn't get updated when I change the map.
I stuck with this issue for a day and managed to come up with a minimum example that demonstrates the issue:
<script setup>
import {computed, reactive, ref, watch} from "vue";
const map = reactive({});
const key = "key";
const mapContainsKeyComputed = computed(() => map.hasOwnProperty(key))
const mapContainsKeyWatched = ref(map.hasOwnProperty(key));
watch(map, () => mapContainsKeyWatched.value = map.hasOwnProperty(key))
</script>
<template>
Map: {{map}}
<br/>
Computed: does map contain "key"? {{mapContainsKeyComputed}}
<br/>
Watch: does map contain key? {{mapContainsKeyWatched}}
<br/>
<button #click="map[key] = 'value'">add key-value</button>
</template>
I've read a bunch of stackoverflow answers and the Vue docs, but I still can't figure it out.
why mapContainsKeyComputed doesn't get updated?
if the reactive doesn't "track" adding or removing keys to the map, why the Map: {{map}} (line 14) updates perfectly fine?
when I replace the map{} with an array[] and "hasOwnProperty" with "includes()", it works fine. How's that different?
how do I overcome this issue without the ugly "watch" solution where the "map.hasOwnProperty(key)" has to be duplicated?
EDIT: as mentioned by #estus-flask, this was a VueJS bug fixed in 3.2.46.
Vue reactivity needs to explicitly support reactive object methods. hasOwnProperty is rather low-level so it hasn't been supported for some time. Without the support, map.hasOwnProperty(key) tries to access key on non-reactive raw object and doesn't trigger the reactivity, so the first computed call doesn't set a listener that could be triggered with the next map change.
One way this could be fixed is to either define key initially (as suggested in another answer), this is the legacy way to make reactivity work in both Vue 2 and 3:
const map = reactive({ key: undefined })
Another way is to access missing key property on reactive object:
const mapContainsKeyComputed = computed(() => map[key] !== undefined)
Yet another way is to use in operator. Since Vue 3 uses Proxy for reactivity, that a property is accessed can be detected by has trap:
const mapContainsKeyComputed = computed(() => key in map)
The support for hasOwnProperty has been recently added in 3.2.46, so the code from the question is supposed to be workable with the latest Vue version.
map is not really a map. This would be different in any Vue 3 version if Map were used, it's supported by Vue and it's expected that map.has(key) would trigger reactivity.

Pinia: $reset alternative when using setup syntax

I have a pinia store created with setup syntax like:
defineStore('id', () => {
const counter = ref(0)
return { counter }
})
Everything has been working great with setup syntax because I can re-use other pinia stores.
Now, however, I see the need to re-use Pinia stores on other pages but their state needs to be reset.
In Vuex for example, I was using registerModule and unregisterModule to achieve having a fresh store.
So the question is: How to reset the pinia store with setup syntax?
Note: The $reset() method is only implemented for stores defined with the object syntax, so that is not an option.
Note 2: I know that I can do it manually by creating a function where you set all the state values to their initial ones
Note 3: I found $dispose but it doesn't work. If $dispose is the answer, then how it works resetting the store between 2 components?
You can use a Pinia plugin that adds a $reset() function to all stores:
On the Pinia instance, call use() with a function that receives a store property. This function is a Pinia plugin.
Deep-copy store.$state as the initial state. A utility like lodash.clonedeep is recommended for state that includes nested properties or complex data types, such as Set.
Define store.$reset() as a function that calls store.$patch() with a deep-clone of the initial state from above. It's important to deep-clone the state again in order to remove references to the copy itself.
// store.js
import { createPinia } from 'pinia'
import cloneDeep from 'lodash.clonedeep'
const store = createPinia()
1️⃣
store.use(({ store }) => {
2️⃣
const initialState = cloneDeep(store.$state)
3️⃣
store.$reset = () => store.$patch(cloneDeep(initialState))
})
demo
Feel free to read the article based on this answer: How to reset stores created with function/setup syntax
You can do this as suggested in the documentation here
myStore.$dispose()
const pinia = usePinia()
delete pinia.state.value[myStore.$id]

Vue3 equivalent to create component instance via new Vue()

I have a data-modelling library that uses underlying Vue2 component instances, eg:
const Person = Vue.extend({
name: 'Person',
//- ... data, computed, watch, methods, events, etc...
})
const person = new Person({ ... })
I can't find docs to determine if there is an equivalent method of creating a new component instance detached from the DOM and OUTSIDE of the main App's context.
Certainly createApp() isn't what I'm looking for - from the Vue3 API Docs
I guess defineComponent() seems to be the way to define the component (d'uh!) ... but since there is no constructor, there seems to be no way to instantiate it outside of the Apps context (ie mounting it)
Is there such a thing? Have I perhaps been versioned-out of my sweet sweet data modelling library?!? Tell me it's not so!

Reuse components with different Vuex stores in NuxtJS - Create dynamic/multiple VueX store instances

I have a vue.js/nuxt.js component in my UI that displays news based on a backend which can be queried with selectors (e.g. news-type1, news-type2).
I want to add a second instance of that component which uses exactly the same backend, but allows the user to use a few different selectors (e.g. news-type3, news-type4). The UI kinda works dashboard-like. Implementing that distinction in the .vue component file is no problem (just accept some props and display stuff conditionally to the user), but:
How do I reuse the vuex store? The code of the store for the new card stays exactly the same since the same backend is used. But I can't use the same instance of the store because the selectors and the loaded news should be stored per component and should not be shared between them. Surprisingly I haven't been able to find any easy solutions for that in nuxt. I thought this would be a common use case.
MWE for my use case:
/** Vuex store in store/news.js **/
export const state = () => ({
// per default only news-type1 is selected, but not news-type2. the user can change that in the UI
currentSelectors: ['news-type1'],
news = [] // object array containing the fetched news
});
export const mutations = {
// some very simple mutations for the stae
setSelectors (state, data) {
state.currentSelectors = data;
},
setNews (state, data) {
state.news = data;
}
}
export const actions = {
// simplified get and commit function based on the currentSelectors
async loadNews ({ commit, state }) {
const news = await this.$axios.$get(`/api/news/${state.currentSelectors.join(',')}`);
commit('setNews', news);
// ... truncated error handling
},
// Helper action. In comparison to the mutation with the same name, it also calls the load action
async setSelectors ({ commit, dispatch }, selectors) {
commit('setSelectors', selectors);
dispatch('loadNews');
},
};
In my news-card.vue I simply map the two states and call the two actions loadNews (initial load) and setSelectors (after user changes what news to show in the UI). This should stay the same in both instances of the card, it just should go to different store instances.
My current alternative would be to simply copy-paste the store code to a new file store/news-two.js and then using either that store or the original store depending on which prop is passed to my news-card component. For obvious reasons, that would be bad practice. Is there a better complicated alternative that works with nuxt?
All related questions I have found are only for Vue, not for nuxt vuex stores: Need multiple instances of Vuex module for multiple Vue instances or How to re-use component that should use unique vuex store instance.

How to access Vuex modules mutations

I read thorugh his documentation from vue but I didn't find anything about how to actually access specific module in the store when you have multiple modules.
Here is my store:
export default new Vuex.Store({
modules: {
listingModule: listingModule,
openListingsOnDashModule: listingsOnDashModule,
closedListingsOnDashModule: listingsOnDashModule
}
})
Each module has its own state, mutations and getters.
state can be successfully accessed via
this.$store.state.listingModule // <-- access listingModule
The same is not true for accessing mutations cause when I do this
this.$store.listingModule.commit('REPLACE_LISTINGS', res)
or
this.$store.mutations.listingModule.commit('REPLACE_LISTINGS', res)
I get either this.$store.listingModule or this.$store.mutations undefined error.
Do you know how should the module getters and mutations be accessed?
EDIT
As Jacob brought out, the mutations can be accessed by its unique identifier. So be it and I renamed the mutation and now have access.
here is my mutation:
mutations: {
REPLACE_OPEN_DASH_LISTINGS(state, payload){
state.listings = payload
},
}
Here is my state
state: {
listings:[{
id: 0,
location: {},
...
}]
}
As I do a commit with a payload of an array the state only saves ONE element.
Giving in payload array of 4 it returns me back array of 1.
What am I missing?
Thanks!
It's a good idea, IMHO, to call vuex actions instead of invoking mutations. An action can be easily accessed without worrying about which module you are using, and is helpful especially when you have any asynchronous action taking place.
https://vuex.vuejs.org/en/actions.html
That said, as Jacob pointed out already, mutation names are unique, which is why many vuex templates/examples have a separate file called mutation-types.js that helps organize all mutations.
re. the edit, It's not very clear what the issue is, and I would encourage you to split it into a separate question, and include more of the code, or update the question title.
While I can't tell why it's not working, I would suggest you try using this, as it can resolve two common issues
import Vue from 'vue'
//...
mutations: {
REPLACE_OPEN_DASH_LISTINGS(state, payload){
Vue.$set(state, 'listings', [...payload]);
},
}
reactivity not triggered. Using Vue.$set() forces reactivity to kick in for some of the variables that wouldn't trigger otherwise. This is important for nested data (like object of an object), because vue does not create a setter/getter for every data point inside an object or array, just the top level.
rest destructuring. Arrays: [...myArray] Objects: {...myObj}. This prevents data from being changed by another process, by assigning the contents of the array/object as a new array/object. Note though that this is only one level deep, so deeply nested data will still see that issue.