Composition API | Vue route params not being watched - vue.js

I have pretty much the same example from the docs of watch and vue router for composition API. But console.log is never getting triggered even though the route.params.gameId is correctly displayed in template
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
const store = useStore()
const route = useRoute()
watch(
() => route.params.gameId,
async newId => {
console.log("watch"+newId)
}
)
</script>
<template>
<div>
{{route.params.gameId}}
</div>
</template>
What am I doing wrong? I also tried making watch function non async but it didn't change anything and later on I will need it for api fetching so it should be async.

You should add immediate: true option to your watch :
watch(
() => route.params.gameId,
async newId => {
console.log("watch"+newId)
},
{
immediate: true
}
)
Because the watch doesn't run at the first rendering while the params is changes only one time at the page load, So the watch misses that change.

Related

The data from Pinia store is not reactive in Nuxt 3 when switching language

I'm just starting with Nuxt and the answer could be obvious, but I'm hoping to get support from you.
I've got a 2 language website, built with Nuxt 3 that uses Nuxt I18n for internationalization, which retrieves data from an API (a strapi headless cms). I've managed to set up a Pinia store in order to not overuse the API, which looks like this:
// /stores/store.js
import { defineStore } from "pinia";
import { useFetch } from "#app";
export const useStore = defineStore("store", {
state: () => ({
data: {
en: [],
ru: []
}
}),
actions: {
async fetchData() {
let resEn = await useFetch('strapi-url.com/api/data', {
params: {
locale: 'en'
}
});
if (resEn.error.value) {
throw createError({
statusCode: resEn.error.value.statusCode,
statusMessage: resEn.error.value.statusMessage
});
}
this.data.en = resEn.data;
let resFr = await useFetch('strapi-url.com/api/data', {
params: {
locale: 'fr'
}
});
if (resFr.error.value) {
throw createError({
statusCode: resFr.error.value.statusCode,
statusMessage: resFr.error.value.statusMessage
});
}
this.data.fr = resFr.data;
}
}
});
And to make the data available when app loads I've setup the app.vue file:
<script setup>
import { useStore } from "~/stores/store";
const store = usetStore();
await store.fetchData();
</script>
<template>
<div>
<Header/>
<NuxtPage/>
<Footer/>
</div>
</template>
and then in a component (ex: Header.vue) I'm getting the data from the store an render it:
<script setup>
import { useStore } from "~/stores/NewsletterStore";
import { storeToRefs } from "pinia";
const { locale } = useI18n();
const store = useStore();
const { data } = storeToRefs(store);
const title = data[locale].title;
</script>
<template>
<div>
{{ title }}
</div>
</template>
The problem is that when the language changes, by a locale switcher, the data isn't refreshed, even if the locale changes too.
I would like to know if there's any way to make it reactive, based on the selected locale.
Thanks & looking forward.
I've tried to setup a pinia store using nuxt 3 web app that has 2 languages controlled by Nuxt I18n module that consumes data from an strapi backend API, but the data rendered isn't reactive when changing locale. I expect to know how to make this data be reactive, when language changes?

Can I use watch from Vue 3 with primitives?

Every time I fetch data, I want to change boolean value to render <Loading /> component.
I don't want my condition to be dependant on array length. So I decided to do it this way.
And <Loading /> component never reacts to state.isLoading change.
I tried to test whether this.isLoading changes at all using watch. But watch never logged anything.
I've never seen anybody using watch with primitives.
The problem is that I don't know if I can use watch with primitives and what I can use instead, like useEffect in React.
App.vue
<script setup>
import { RouterView } from 'vue-router'
import { watch, ref, onMounted, reactive } from 'vue';
import Navbar from './components/Navbar.vue'
import { useShopStore } from './stores/shopStore';
const shop = useShopStore()
const bool = ref(shop.isLoading)
console.log(bool)
watch(bool.value, (newBool) => {
console.log(newBool)
}, { deep: true })
</script>
Category.vue
<template>
<LoadingVue v-if="shop.isLoading" />
<div v-else class="category__menu">
<CardVue
v-for="item in shop.category"
:item="item"
:key="item.id"
/>
</div>
</template>
ShopStore.js
actions: {
async getProducts(path) {
if (typeof path !== 'string' || path === undefined) return
this.setLoading()
try {
const response = fetch(`https://fakestoreapi.com/products/category/${path}`)
.then(res => res.json())
.then(res => this.category = res)
} catch (error) {
console.log(error)
alert('Something went wrong')
}
this.setLoading()
},
setLoading() {
console.log('setLoading')
this.isLoading = !this.isLoading
}
}
You are creating a new ref over a reactive data. It's like copying by value, the original reactive data and the new ref wrapped over it are not connected. So when shop.isLoading changes, your bool ref doesn't, they are two different variables now.
I guess you are using pinia for the store. If so, the shop.isLoading is already reactive, you don't have to wrap it into a ref.
<Loading v-model="shop.isLoading" />
You can also use storeToRefs helper method from pinia to use destructuring over your store and get refs of your state:
const { isLoading } = storeToRefs(shop)
console.log(isLoading.value)
So.
The problem was that I used async but I didn't use await inside the function and that's why condition worked the way it worked. Or didn't work as I expected.
Now I fixed it and I want to publicly admit that I am a complete moron.
Thank you for your attention.
P.S.
Still didn't figure out how to use watch. The only way is to watch the whole state object. watch doesn't react to only state.bool value change.

How Do I Share Async API Data across Components in Vue 3?

I have a top-level component that gets data from an API at regular intervals. I want to make a single API request and get all the data for my app in one place to reduce the number of requests to the API server. (FYI, my project looks like it's using Typescript but I'm not yet.)
Everything works fine in my top-level component:
//Parent
<script lang="ts">
import { defineComponent, ref, provide, inject, onMounted } from 'vue'
import getData from '#/data.ts'
export default defineComponent({
setup(){
const workspaces = ref([])
onMounted(async () => {
let api = inject('api') //global var from main.ts
let data = await getData(api) //API request inside data.ts
console.log(data.workspaces) //<-- data looks good here
workspaces.value = data.workspaces
//Trying to share workspaces with other components
provide('workspaces', data.workspaces)
})
return {
workspaces
}
}
})
</script>
<template>
{{ workspaces}} <!-- workspaces render fine here -->
</template>
But my child can't use the provide data via inject:
//Child
<script lang="ts">
import { defineComponent, inject, onMounted, ref } from 'vue'
export default defineComponent({
setup(){
let workspaces = ref([])
onMounted(async () => {
workspaces.value = await inject('workspaces') //<-- just a guess; doesn't work
})
return{
workspaces
}
}
})
</script>
<template>
{{ workspaces }} <!-- nothing here -->
</template>
I've made a couple assumptions as to the cause of the problem:
The Child component loads before the parent's async stuff is done, and is therefore empty.
I probably can't use project/inject in async scenarios like this.
So how can I share async data from an API across components in my app? Is my only option to go back to old-school props and pass the data down manually?
provide/inject are misused and subject to race conditions. Composition API is generally supposed to be at used on component initialization (setup, before any await) and not in onMounted. Even if there weren't such restriction, onMounted in parent component runs after the one in child component and can't provide a value at the time when a child is mounted.
The purpose of refs is to provide a reference to a value that can be changed later, so it could be passed by reference and stay reactive, this property isn't currently used.
It should be in parent component:
setup(){
const workspaces = ref([])
provide('workspaces', workspaces)
let api = inject('api')
onMounted(async () => {
let data = await getData(api)
workspaces.value = data.workspaces
})
return { workspaces }
In child component:
setup(){
let workspaces = inject('workspaces')
return { workspaces }

Watch child properties from parent component in vue 3

I'm wondering how I can observe child properties from the parent component in Vue 3 using the composition api (I'm working with the experimental script setup).
<template>//Child.vue
<button
#click="count++"
v-text="'count: ' + count"
/>
</template>
<script setup>
import { ref } from 'vue'
let count = ref(1)
</script>
<template>//Parent.vue
<p>parent: {{ count }}</p> //update me with a watcher
<Child ref="childComponent" />
</template>
<script setup>
import Child from './Child.vue'
import { onMounted, ref, watch } from 'vue'
const childComponent = ref(null)
let count = ref(0)
onMounted(() => {
watch(childComponent.count.value, (newVal, oldVal) => {
console.log(newVal, oldVal);
count.value = newVal
})
})
</script>
I want to understand how I can watch changes in the child component from the parent component. My not working solution is inspired by the Vue.js 2 Solution asked here. So I don't want to emit the count.value but just watch for changes.
Thank you!
The Bindings inside of <script setup> are "closed by default" as you can see here.
However you can explicitly expose certain refs.
For that you use useContext().expose({ ref1,ref2,ref3 })
So simply add this to Child.vue:
import { useContext } from 'vue'
useContext().expose({ count })
and then change the Watcher in Parent.vue to:
watch(() => childComponent.value.count, (newVal, oldVal) => {
console.log(newVal, oldVal);
count.value = newVal
})
And it works!
I've answered the Vue 2 Solution
and it works perfectly fine with Vue 3 if you don't use script setup or explicitly expose properties.
Here is the working code.
Child.vue
<template>
<button #click="count++">Increase</button>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
return {
count: ref(0),
};
},
};
</script>
Parent.vue
<template>
<div id="app">
<Child ref="childComponent" />
</div>
</template>
<script>
import { ref, onMounted, watch } from 'vue';
import Child from './components/Child.vue';
export default {
components: {
Child,
},
setup() {
const childComponent = ref(null);
onMounted(() => {
watch(
() => childComponent.value.count,
(newVal) => {
console.log({ newVal }) // runs when count changes
}
);
});
return { childComponent };
},
};
</script>
See it live on StackBlitz
Please keep reading
In the Vue 2 Solution I have described that we should use the mounted hook in order to be able to watch child properties.
In Vue 3 however, that's no longer an issue/limitation since the watcher has additional options like flush: 'post' which ensures that the element has been rendered.
Make sure to read the Docs: Watching Template Refs
When using script setup, the public instance of the component it's not exposed and thus, the Vue 2 solutions will not work.
In order to make it work you need to explicitly expose properties:
With script setup
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
With Options API
export default {
expose: ['publicData', 'publicMethod'],
data() {
return {
publicData: 'foo',
privateData: 'bar'
}
},
methods: {
publicMethod() {
/* ... */
},
privateMethod() {
/* ... */
}
}
}
Note: If you define expose in Options API then only those properties will be exposed. The rest will not be accessible from template refs or $parent chains.

Vuex is resetting already set states

Have started to play around with Vuex and am a bit confused.
It triggers the action GET_RECRUITERS everytime I load the component company.vue thus also making an api-call.
For example if I open company.vue => navigate to the user/edit.vue with vue-router and them go back it will call the action/api again (The recruiters are saved in the store accordinly to Vue-dev-tools).
Please correct me if I'm wrong - It should not trigger the action/api and thus resetting the state if I go back to the page again, correct? Or have I missunderstood the intent of Vuex?
company.vue
<template>
<card>
<select>
<option v-for="recruiter in recruiters"
:value="recruiter.id">
{{ recruiter.name }}
</option>
</select>
</card>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
middleware: 'auth',
mounted() {
this.$store.dispatch("company/GET_RECRUITERS")
},
computed: mapGetters({
recruiters: 'company/recruiters'
}),
}
</script>
company.js
import axios from 'axios'
// state
export const state = {
recruiters: [],
}
// getters
export const getters = {
recruiters: state => {
return state.recruiters
}
}
// actions
export const actions = {
GET_RECRUITERS(context) {
axios.get("api/recruiters")
.then((response) => {
console.log('API Action GET_RECRUITERS')
context.commit("GET_RECRUITERS", response.data.data)
})
.catch(() => { console.log("Error........") })
}
}
// mutations
export const mutations = {
GET_RECRUITERS(state, data) {
return state.recruiters = data
}
}
Thanks!
That's expected behavior, because a page component is created/mounted again each time you route back to it unless you cache it. Here are a few design patterns for this:
Load the data in App.vue which only runs once.
Or, check that the data isn't already loaded before making the API call:
// Testing that your `recruiters` getter has no length before loading data
mounted() {
if(!this.recruiters.length) {
this.$store.dispatch("company/GET_RECRUITERS");
}
}
Or, cache the page component so it's not recreated each time you route away and back. Do this by using the <keep-alive> component to wrap the <router-view>:
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>