useFetch in onMounted not fetching data on direct link open nuxt3 - vue.js

I'm using the useFetch to fetch the data in the composition api and then calling the function in onMounted hook in components, here is the code.
useShows.ts (composable)
export function useShows(){
var shows = useState<Show[]>('shows')
const fetchShows = async() => {
const {data, pending} = await useFetch<Show[]>('http://localhost:3000/shows')
shows.value = data.value
}
return {shows, fetchShows}
}
shows.vue
<script setup lang="ts">
var { shows, fetchShows } = useShows()
onMounted(() => {
console.log("On mounted called")
fetchShows()
})
</script>
<template>
<div>{{shows}}</div>
</template>
When I'm navigating to /shows from the home page it is working fine, but when I direct open the link localhost/shows it is not working and only giving me the null.

I have the exact same problem. I solved it by wrapping it within a nextTick.
await nextTick(async () => {
await fetchShows()
})

Related

vuejs 3 composition api getCurrentInstance($refs)

I have a problem
I can access $ref in onmounted function but
note : getCurrentInstance imported
I am getting the following error in submit function
Uncaught TypeError: Cannot read properties of null (reading 'ctx')
const submit = () => {
getCurrentInstance().ctx.$refs.modalLoading.openModal()
}
onMounted(() => {
getCurrentInstance().ctx.$refs.modalLoading.closeModal()
})
As getCurrentInstance should normally not been used, you should follow the Template Refs guide.
<!—- inside the template —->
<my-modal ref="modalLoading" />
// your component
export default defineComponent({
setup() {
const modalLoading = ref(null);
const handleSubmit = () => {
If(!modalLoading.value) return;
modalLoading.value.openModal();
}
return { modalLoading };
}
})

How can I access context from a <script setup> tag in vue3?

Previously I used the standard < script > tag with the setup function within it:
<script>
import { ref } from 'vue'
import useLogin from '../composables/useLogin'
const email = ref('')
const password = ref('')
export default {
setup(props, context){
const {error, login} = useLogin()
const handleLoginSubmit = async () => {
await login(email.value, password.value)
if(!error.value){
context.emit('login')
}
router.push({name: 'home'})
}
return { email, password, handleLoginSubmit }
}
}
</script>
Now I tried to switch to the < script setup > tag, but I don't know how to access the context attribute here.
<script setup>
import { ref } from 'vue'
import useLogin from '../composables/useLogin'
import router from '../router'
const email = ref('')
const password = ref('')
const {error, login} = useLogin()
const handleLoginSubmit = async () => {
await login(email.value, password.value)
if(!error.value){
context.emit('login')
}
router.push({name: 'home'})
}
</script>
I get the following error due to the missing definition of the context.
Uncaught ReferenceError: context is not defined
I tried to import context from vue in different ways, but nothing seems to be the correct.
Context is not available in <script setup> if you want to use emits and props, there are some helpers(macros) exposed to you.
In your case you are trying to use emit, so in script setup it is going to be something like:
<script setup>
const emit = defineEmits(['event1','event2']);
...
emit('event1','some-value');
</script>
So, in your example, that would be:
const emit = defineEmits(['login']);
const handleLoginSubmit = async () => {
await login(email.value, password.value)
if(!error.value){
emit('login')
}
router.push({name: 'home'})
}
For props, you use defineProps. See this for more info

How to access a injected repository from a component's method

Lets say we injected this repository on a plugin/service-container.js
import nodeFetch from 'node-fetch'
import { AbortController as NodeAbortController } from 'node-abort-controller'
import HttpClient from '#/services/httpClient'
import PostRepository from '#/repositories/posts'
export default ({ app }, inject) => {
if (!process.client || app.context.env.NUXTJS_DEPLOY_TARGET === 'server') {
inject('postRepository', postRepository)
}
}
I have always acceded to API repositories from the asyncData method, like so:
export default {
async asyncData ({ $postRepository, }) {
const posts = await $postRepository.getAllPaginated(page, 11)
return {
posts,
}
}
}
But I need to access to it in a method, this is actually working but:
I doesn't look the right way because i'm caching in the component's data()
It fires this lint error:
Async method 'asyncData' has no 'await' expression.eslintrequire-await
What's the right way? I Can't find it online (the only examples I found involved using the Store)
export default {
async asyncData ({ $postRepository }) {
this.$postRepository = $postRepository
},
methods: {
async loadMore () {
if (this.page < this.posts.numPages) {
const posts = await this.$postRepository.getAllPaginated(this.page + 1, 11)
}
}
}
}
The error is coming from here
async asyncData ({ $postRepository }) {
this.$postRepository = [missing await here] $postRepository
},
From the documentation
This hook can only be used for page-level components. Unlike fetch, asyncData cannot access the component instance (this). Instead, it receives the context as its argument. You can use it to fetch some data and Nuxt will automatically shallow merge the returned object with the component data.
Hence, you cannot use any kind of this.loadMore in asyncData because it doesn't have access to the instance yet. So, inject is indeed the proper way of doing things.
With a plugin like that
export default ({ _ }, inject) => {
inject('customTest', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
return await response.json()
})
}
And a page like this
<template>
<div>
<pre>item: {{ item }}</pre>
</div>
</template>
<script>
export default {
async asyncData({ $customTest }) {
const item = await $customTest()
return { item }
},
}
</script>
It is not calling a method but you could totally use this.$nuxt.refresh() to fetch it again and increment the index of the repository call after an update in the store.
Which could be referenced like
await fetch(`https://jsonplaceholder.typicode.com/todos/${indexFromVuex}`)
You could of course keep it local too
<template>
<div>
<pre>item: {{ item }}</pre>
<button #click="fetchNewItem">fetch new item</button>
</div>
</template>
<script>
export default {
async asyncData({ $customTest }) {
const item = await $customTest()
return { item }
},
data() {
return {
index: 1,
}
},
methods: {
async fetchNewItem() {
this.index += 1
this.item = await this.$customTest(this.index)
},
},
}
</script>
So yeah, I don't think that there are other possible approaches with asyncData.
The fetch() hook is a bit more flexible but it's also totally different too regarding how it is working.
Anyway, with those 2 approaches you could totally have enough to solve the issue of your HTTP call.
It seems that an injected dependency can be accessed (in this case) with simply this.$postRepository inside any method so I didn't even need that asyncData

AsyncData with multiple requests in Nuxt

I'm trying to get the answer from two API routes and depending on the result display the data. But for some reason, when I trying to use more than 1 axios call it doesn't work, failing with 404/500 error.
I've tried following:
<template>
<div v-if="blogPost">
<p>post</p>
</div>
<div v-else-if="blogCategoryPosts">
<p>category,posts</p>
</div>
</template>
<script>
export default {
async asyncData({ $axios, app, route }) {
const blogPost = await $axios.get(`${process.env.API_DOMAIN}/api/blog/posts${route.path}`)
const blogCategoryPosts = await $axios.get(`${process.env.API_DOMAIN}/api/blog/categories${route.path}`)
return {
blogPost: blogPost.data,
blogCategoryPosts: blogCategoryPosts.data,
}
},
}
</script>
and
<script>
export default {
async asyncData({ $axios, app, route}) {
const [blogPost, blogCategoryPosts] = await Promise.all([
$axios.get(`${process.env.API_DOMAIN}/api/blog/posts${route.path}`),
$axios.get(`${process.env.API_DOMAIN}/api/blog/categories${route.path}`),
])
return {
blogPost: blogPost.data,
blogCategoryPosts: blogCategoryPosts.data,
}
},
}
</script>
Each call works fine separately but together they don't. Any idea why is that?
You should await your Promise.all like this
const [blogPost, blogCategoryPosts] = await Promise.all([
$axios.get(`${process.env.API_DOMAIN}/api/blog/posts${route.path}`),
$axios.get(`${process.env.API_DOMAIN}/api/blog/categories${route.path}`),
])
Also, don't forget the , at the end of the first $axios.
I gave a similar answer here few time ago.
PS: if you want to have those issues fixed quickly, use ESlint.
If you want a tutorial on how to have both ESlint and Prettier, you can follow my tutorial here.
So in my case it was sufficient to point on .then and .catch for axios
export default {
async asyncData({ $axios, app, route}) {
const blogPost = await $axios.get(`${process.env.API_DOMAIN}/api/blog/posts${route.path}`).then(response => response.data).catch(error => {})
const blogCategoryPosts = await $axios.get(`${process.env.API_DOMAIN}/api/blog/categories${route.path}`).then(response => response.data).catch(error => {})
return {
blogPost: blogPost,
blogCategoryPosts: blogCategoryPosts,
}
},
}
Everything worked well. Also I sort of misunderstood 500 error, i thought it a generic message, but my API was just telling me that category not found.

Dynamic layout in Vue 3 with Vite

New to Vue and Vite but trying to get dynamic layouts working properly here. I believe I have what is needed but the issue it the meta seems to always come up as an empty object or undefined.
AppLayout.vue
<script setup lang="ts">
import AppLayoutDefault from './stub/AppLayoutDefault.vue'
import { markRaw, watch } from 'vue'
import { useRoute } from 'vue-router'
const layout = markRaw(AppLayoutDefault)
const route = useRoute()
console.log('Current path: ', route.path)
console.log('Route meta:', route.meta)
watch(
() => route.meta,
async (meta) => {
try {
const component = await import(`./stub/${meta.layout}.vue`)
layout.value = component?.default || AppLayoutDefault
} catch (e) {
layout.value = AppLayoutDefault
}
},
{ immediate: true }
)
</script>
<template>
<component :is="layout"> <router-view /> </component>
</template>
App.vue
<script setup lang="ts">
import AppLayout from '#/layouts/AppLayout.vue'
</script>
<template>
<AppLayout>
<router-view />
</AppLayout>
</template>
Each and every route has the appropriate meta set with a property called layout.
I just can't seem to get he layout applied correctly on the first load or any click of a link in the navbar(which are just router-link) for that matter.
The main problem is layout is initialized as markRaw(), which is not a reactive ref:
const layout = markRaw(AppLayoutDefault) // ❌ not reactive
Solution
Initialize layout as a ref.
Instead of watching the full route object, watch route.meta?.layout because that's the only relevant field for the handler.
Wrap layout's new values with markRaw() to avoid reactivity on the component definition.
const layout = ref() 1️⃣
watch(
() => route.meta?.layout as string | undefined, 2️⃣
async (metaLayout) => {
try {
const component = metaLayout && await import(/* #vite-ignore */ `./${metaLayout}.vue`)
layout.value = markRaw(component?.default || AppLayoutDefault) 3️⃣
} catch (e) {
layout.value = markRaw(AppLayoutDefault) 3️⃣
}
},
{ immediate: true }
)
demo
The solution from Tony is great. But you need to add computed inside the watch because it will prevent code execution for the very first "layout" variable initialized which is still undefined and it will cause unnecessary rendering. So, please add the computed function inside the watch function. Also you need to watch "route.path" not the "route.meta?.layout".
import DefaultLayout from '#/layouts/Default.vue'
import { markRaw, ref } from '#vue/reactivity'
import { computed, watch } from '#vue/runtime-core'
import { useRoute } from 'vue-router'
const layout = ref()
const route = useRoute()
watch(
computed(() => route.path), async () => {
let metaLayout = route.meta?.layout
try {
const metaLayoutComponent = metaLayout && await import(`./layouts/${metaLayout}.vue`)
layout.value = markRaw(metaLayoutComponent?.default || DefaultLayout)
} catch (error) {
layout.value = markRaw(DefaultLayout)
}
}
);