Nesting Vue composables - vue.js

I'm working on a codebase that relies heavily on the #vue/composition-api. There are a few places in the code where computed and watch calls are nested. An example:
const refA = ref<number | null>(null);
const refB = ref<number | null>(null);
const onClick = () => {
refA.value = 5;
};
watch(refA, (newValue) => {
if (newValue === null) {
return;
}
const {
result,
} = useSomeComposable(newValue);
watch(result, (newResultValue) => {
refB.value = newResultValue;
});
});
My questions is, if it is a recommended way, or composables should not be nested? Are there any downsides of doing this? E.g. callback not getting cleaned up by the GC because of some refs? Is there an easy way of checking if temporary refs within watch / computed callbacks are still in the memory?

Related

Vue 3 / Pinia: how to get the updated value from the store when it's changed?

Using Vue 3 with Pinia in an Interia setup. I can see Pinia in my Dev Tools and I can console log out the (starting) value.
I have a component set up like:
<Comment>
<ChildComment>
In ChildComment, I have this function
const toggleEdit = () => {
commentStore.childEditing = !editMode.value
editMode.value = !editMode.value;
};
When I trigger this function, the value updates in the store:
In the <Comment> component, I have tried using various methods like so:
import { useStore } from "../../store/commentStore";
const commentStore = useStore();
// USING STATE DIRECTLY
// const isEditingChildComment = ref(commentStore.childEditing);
// const isEditingChildComment = reactive(commentStore.childEditing);
// const isEditingChildComment = computed(() => commentStore.childEditing);
// USING GETTER
// const isEditingChildComment = ref(commentStore.getChildEditing);
// const isEditingChildComment = reactive(commentStore.getChildEditing);
// const isEditingChildComment = computed(() => commentStore.getChildEditing);
But isEditingChildComment is never updated in any of the above 6 scenarios, it remains as false:
Am I missing something obvious?
If you use a computed property, you need to return a value. For example:
const isEditingChildComment = computed(() => {
return commentStore.childEditing);
}
It looks also if you are mutating the store state directly. I think you should use an Action for that, to guarantee the working of reactivity.
Try using the store.$subscribe function. Here's a link to the docs
Example from docs:
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// same as cartStore.$id
mutation.storeId // 'cart'
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to cartStore.$patch()
// persist the whole state to the local storage whenever it changes
localStorage.setItem('cart', JSON.stringify(state))
})

Vue JS composition API can't access array

I am currently trying to learn Vue JS without ever having encountered Javascript.
All the brackets, arrows, etc. are driving me crazy.
With the Composition API, I come across a question that I can't successfully google.
That's working:
setup() {
const store = useStore ();
const packagingdata = ref ([]);
const loadpackagingdata = async () => {
await store.dispatch (Actions.LOADPACKAGING_LIST, 10);
packagingdata.value = store.getters.getPackagingData;
return {
packagingdata,
}
I can access {{packagingdata}}. That contains an array.
{{Packagingdata.products}}
does work
but {{packagingdata.products [0]}} doesn't work.
But when I add it to the setup () like this:
setup() {
const store = useStore ();
const packagingdata = ref ([]);
const getProducts = ref ([]);
const loadpackagingdata = async () => {
await store.dispatch (Actions.LOADPACKAGING_LIST, 10);
packagingdata.value = store.getters.getPackagingData;
getProducts.value = store.getters.getProducts;
};
return {
packagingdata,
getProducts
}
then {{ getProducts }} returns what I wanted even if the getter function only is like this:
get getAddress() {
return this.packagingdata["products"][0];
}
What is happening there?
What am I doing wrong? I would prefer to not create a ref() and getterfunction for every computed value.
Whats the solution with computed?
best regards
I found two methods to avoid the error.
add a v-if="mygetter.length" in the template
check in the getter whether the variable is set and otherwise return false
Both of these prevent an error already occurring when the page is loaded that [0] cannot be found

React Native Hooks initializer not taking the correct value

What I am trying to do is sync a list of attendees from an online database, and if the current user is in the list, then disable a button, else enable the button.
I am using react native hook (I am not sure if I am using the term correctly as I am fairly new to react), in order to set the value of disabling the button.
The issue that I am facing is that the value is getting initialized to false, even tho it should clearly get initialized to true.
After adding some logging I made sure that the function is executing correctly and reaching the code where it sets the value to true.
const [buttonDisabled, changeButtonState] = useState( () => {
var database = firebase.database();
var userId = firebase.auth().currentUser.uid;
const dbRef = firebase.database().ref();
var Attendees = [];
var disable = false;
dbRef.child("gameAttendees").child(gameinfo.gameID).get().then((snapshot) => {
if (snapshot.exists()) {
Attendees = snapshot.val().Attendees;
for(var i=0;i<Attendees.length;i++){
if(Attendees[i]==userId){
return true;
}
}
} else {
console.log("no value");
return false;
}
}).catch((error) => {
console.error(error);
});
});
Adding an example of an async mount effect:
const Comp = () => {
const [s, setS] = useState(); // State will be undefined for first n renders
useEffect(() => {
// Call the async function and set the component state some time in the future
someAsyncFunction().then(result => setS(result));
}, []); // An effect with no dependencies will run only once on mount
return </>;
};

reactive object not updating on event emitted from watch

i'm building a complex form using this reactive obj
const formData = reactive({})
provide('formData', formData)
inside the form one of the components is rendered like this:
<ComboZone
v-model:municipality_p="formData.registry.municipality"
v-model:province_p="formData.registry.province"
v-model:region_p="formData.registry.region"
/>
this is the ComboZone render function:
setup(props: any, { emit }) {
const { t } = useI18n()
const { getters } = useStore()
const municipalities = getters['registry/municipalities']
const _provinces = getters['registry/provinces']
const _regions = getters['registry/regions']
const municipality = useModelWrapper(props, emit, 'municipality_p')
const province = useModelWrapper(props, emit, 'province_p')
const region = useModelWrapper(props, emit, 'region_p')
const updateConnectedField = (key: string, collection: ComputedRef<any>) => {
if (collection.value && collection.value.length === 1) {
console.log(`update:${key} => ${collection.value[0].id}`)
emit(`update:${key}`, collection.value[0].id)
} else {
console.log(`update:${key} =>undefined`)
emit(`update:${key}`, undefined)
}
}
const provinces = computed(() => (municipality.value ? _provinces[municipality.value] : []))
const regions = computed(() => (province.value ? _regions[province.value] : []))
watch(municipality, () => updateConnectedField('province_p', provinces))
watch(province, () => updateConnectedField('region_p', regions))
return { t, municipality, province, region, municipalities, provinces, regions }
}
useModelWrapper :
import { computed, WritableComputedRef } from 'vue'
export default function useModelWrapper(props: any, emit: any, name = 'modelValue'): WritableComputedRef<any> {
return computed({
get: () => props[name],
set: (value) => {
console.log(`useModelWrapper update:${name} => ${value}`)
emit(`update:${name}`, value)
}
})
}
problem is that the events emitted from useModelWrapper update the formData in the parent template correctly, the events emitted from inside the watch function are delayed by one render....
TL;DR;
Use watchEffect instead of watch
...with the caveat that I haven't tried to reproduce, my guess is that you're running into this issue because you're using watch which runs lazily.
The lazy nature is a result of deferring execution, which is likely why you're seeing it trigger on the next cycle.
watchEffect
Runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.
watch
Compared to watchEffect, watch allows us to:
Perform the side effect lazily;
Be more specific about what state should trigger the watcher to re-run;
Access both the previous and current value of the watched state.
found a solution, watchEffect wasn't the way.
Looks like was an issue with multiple events of update in the same tick, worked for me handling the flush of the watch with { flush: 'post' } as option of the watch function.
Try to use the key: prop in components. I think it will solve the issue.

Structuring a Vuex module

I am having some trouble in trying to keep my Vuex modules clean and I was hoping to receive some insight on how to improve this. I have already split up some mutations and am using actions to compose multiple mutations so I guess that is a good start.
In most examples I see super clean mutations and I have those as well but a lot I needs checks with if statements or other side effects. To provide examples:
My action:
setFilteredData({ state, commit }, payload) {
commit('setFilteredData', payload);
// Check if we need to split up the data into 'hotels' and 'nearby_hotels'.
if (state.filteredData.find(hotel => hotel.nearby_city)) {
commit('splitHotelsAndNearbyHotels', state.filteredData);
}
}
My mutation:
splitHotelsAndNearbyHotels(state, payload) {
// Chunk it up into hotels and nearby hotels.
const composed = groupBy(payload, 'nearby_city');
if (composed.true) {
composed.true.forEach((hotel) => {
if (hotel.isFirst) hotel.isFirst = false;
});
composed.true[0].isFirst = true;
// Merge them back together in the right order.
state.filteredData = composed.false.concat(composed.true);
}
}
In this example if my array of objects contains a hotel with hotel.nearby_city set to true it will perform the commit of splitHotelsAndNearbyHotels.
The code is not transparent enough. The if statement inside the action does not feel right and I would like my mutation to be cleaner.
I have thought about splitting up my splitHotelsAndNearbyHotels into separate functions but I have no idea where to place those. Simply putting them inside the Vuex file does not feel like a big improvement putting them in a separate file could be an option I guess.
How could I clean up my file to improve the readability? Perhaps someone can show me a Vuex example which does not have an ideal scenario like what I am dealing with.
Actually you can move your actions code into getters, it's more clean to use single source and filter it on getter.
But if you insist using action you can move your mutation code inside on action, and restructure your actions code just like this:
Helper.js
This is for provide data and helper functions:
var _ = require('lodash');
const payloadData = [
{"name":"A", "nearby_city":true, "isFirst":true},
{"name":"B", "nearby_city":false, "isFirst":false},
{"name":"C", "nearby_city":false, "isFirst":false},
{"name":"D", "nearby_city":true, "isFirst":false}
];
// assumed nearby_city is boolean
const isNearby = (hotels) => { return !!hotels.find(hotel => hotel.nearby_city === true) };
const groupBy = (items, key) => { return _.groupBy(items, item => item[key]) };
Mutations.js
This is your mutation looks now:
const mutations = {
setfilteredData : (state, hotels) => {
state.filteredHotels = hotels || [];
},
}
Actions.js
And this is your actions, it's fine without moving your functions into separate files.
// separate filter function
const filterNearby = (payload) => {
if(isNearby(payload) === false){
return payload;
}
const composed = groupBy(payload, 'nearby_city');
composed.true.forEach((hotel) => {
if (hotel.isFirst) hotel.isFirst = false;
});
composed.true[0].isFirst = true;
return composed.false.concat(composed.true);
};
const actions = {
setfilteredData: ({state, commit}, payload) => {
/**
* Using separate filter function
*/
commit('setfilteredData', filterNearby(payload));
return;
/**
* Using restructured code
*/
// Check if we need to split up the data into 'hotels' and 'nearby_hotels'.
if(isNearby(payload) === false){
commit('setfilteredData', payload);
return;
}
// Chunk it up into hotels and nearby hotels.
const composed = groupBy(payload, 'nearby_city');
composed.true.forEach((hotel) => {
if (hotel.isFirst) hotel.isFirst = false;
});
composed.true[0].isFirst = true;
// Merge them back together in the right order.
commit('setfilteredData', composed.false.concat(composed.true));
}
};