Vue 3 Fetching the data and passing to child components - vue.js

I am building a page with several components in it, the data for all these components I need to get by making an ajax call.
As the child components are being mounted before the data comes in I'm getting undefined errors. Whats the best way to pass the data?
Here is a simplified version of what I'm trying to achieve
Stackblitz
In that example I have one Parent.vue and inside that we have 3 child coomponents, ID, Title, Body. After getting the data from API, the child componets are not updating.
Also for making the api calls I am directly calling load method inside setup() is there any better way of doing it?
Code Snippets from the stackblitz link
<template>
<h1>Data from Parent</h1>
{{ post.id }} - {{ post.title }} - {{ post.body }}
<h1>Data from Child</h1>
<IdChild :idP="post.id" />
<TitleChild :titleP="post.title" />
<BodyChild :bodyP="post.body" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const post = ref()
const load = async () => {
let data = await fetch('https://jsonplaceholder.typicode.com/posts/1')
post.value = await data.json()
}
load()
</script>

When dealing with asynchronous data required in your components, you have basically two options:
You render the components before you get the data. It can be with default values or a loading state
<template>
<div v-if="!myAsyncData">Loading...</div>
<div v-else>{{ myAsyncData }}</div>
</template>
<script setup>
const myAsyncData = ref(null)
async function load() {
myAsyncData.value = await /* http call */
}
load() // will start to load and rendering the component
</script>
You await the response in the setup hook combined with the <Suspense> component so it starts to render only when the data is available.
Example (playground):
<!-- App.vue -->
<template>
<Suspense>
<Parent />
</Suspense>
</template>
<!-- Parent.vue -->
<template>
<div>{{ myAsyncData }}</div>
</template>
<script setup>
const myAsyncData = ref(null)
async function load() {
myAsyncData.value = await /* http call */
}
await load() // will wait this before rendering the component. Only works if the component is embebbed within a [`<Suspense>`](https://vuejs.org/guide/built-ins/suspense.html) component.
</script>

Related

How to load data asynchronously inside App.vue during startup before rendering the router-view?

I setup a new Vue using the router project via npm init vue#latest. Before rendering the router-view I must load some data asynchronously and pass it as props to the router-view.
Changing the App.vue file to
<script setup lang="ts">
import { RouterView } from "vue-router";
const response = await fetch("https://dummy.restapiexample.com/api/v1/employees");
const employees = await response.json();
</script>
<template>
<router-view :employees="employees" />
</template>
won't render the current router view and comes up with the warning
[Vue warn]: Component <Anonymous>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered.
at <App>
but my App.vue file does not have any parent, so I can't wrap it inside a suspense tag. But how can I fetch some data before rendering the view? ( And maybe show an error box if something failed instead )
Do I have to create an NestedApp.vue file, just to wrap it inside a suspense tag?
Do I have to come up with something like this?
<script setup lang="ts">
import { RouterView } from "vue-router";
import { ref } from "vue";
const isLoading = ref(true);
const errorOccured = ref(false);
let employees = ref([]);
fetch("https://dummy.restapiexample.com/api/v1/employees")
.then(async response => {
employees = await response.json();
isLoading.value = false;
})
.catch(() => {
errorOccured.value = true;
isLoading.value = false;
});
</script>
<template>
<div v-if="errorOccured">
Something failed!
</div>
<div v-else-if="isLoading">
Still loading!
</div>
<router-view v-else :employees="employees" />
</template>
As a sidenote what I want to do:
The app must be started with an url hash containing base64 encoded data, which is a base url. After extracting and decoding it, I must fetch some data using this url before rendering the router-view.
So maybe there are some better places for this setup code? I thought about the main.ts file but if something fails, I could display an error alert box inside the App.vue file instead.
You can load data in async created(), then use v-if to prevent rendering the dom. (You can show loading screen or spinner instead.)
new Vue({
el: '#app',
data: {
isLoaded: false,
},
async created() {
// load data (async/await)...
await new Promise(r => setTimeout(r, 2000)); // wait 2 sec...
this.isLoaded = true;
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-if="isLoaded">Loaded! -> show data</div>
<div v-else>Loading...</div>
</div>

vue3 js component :is not changing component

I have a component which gathers data from an API. The data brought back from the API is an array with details in. One of the values in the array is the type of component which should be rendered, all other data is passed through to the component.
I'm trying to render the correct component based of the value brought back from the database, but it is sadly not working.
I'm new to Vue but had it working in vue2 but would like it to work in Vue 3 using the composition API.
this is my component code which I want to replace:
<component :is="question.type" :propdata="question" />
When viewed within the browser this is what is actually displayed, but doesn't use the SelectInput component:
<selectinput :propdata="question"></selectinput>
SelectInput is a component with my directory, and works as intended if I hard code the :is value, like below:
<component :is="SelectInput" propdata="question" />
my full component which calls the component component and swaps components:
<template>
<div class="item-group section-wrap">
<div v-bind:key="question.guid" class='component-wrap'>
<div class="component-container">
<!-- working -->
<component :is="SelectInput" :propData="question" />
<!-- not working -->
<component v-bind:is="question.type" :propData="question" />
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, toRefs } from 'vue';
import SelectInput from "./SelectInput";
import TextareaInput from "./TextareaInput";
const props = defineProps({
question: Object,
});
const { question } = toRefs(props);
</script>
In case that your Components filenames are equal to the type specifier on your question Object, then you could dynamically import them to save some code lines.
This would also result in better scalability since you don't have to touch this component anymore in case you create more types.
<template>
<div class="item-group section-wrap">
<div v-bind:key="question.guid" class='component-wrap'>
<div class="component-container">
<!-- working -->
<component v-bind:is="getComponent(question.type)" :propData="question" />
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, toRefs } from 'vue';
const props = defineProps({
question: Object,
});
// GIVEN THE question.types are equal to the fileNames of the components to render:
const getComponent = (name) => import(`./${name}.vue`);
const { question } = toRefs(props);
</script>
Figured out that because I'm using script setup the components aren't named so the component component doesn't know which component to render.
so, i created an object with the components and a reference to the component:
const components = {
'SelectInput': SelectInput,
'TextareaInput': TextareaInput
};
and another function which takes in the component i want to show and links it to the actual component:
const component_type = (type) => components[type];
then in the template i call the function and the correct component is rendered:
<component v-bind:is="component_type(question.type)" :propData="question" />
complete fixed code:
<template>
<div class="item-group section-wrap">
<div v-bind:key="question.guid" class='component-wrap'>
<div class="component-container">
<!-- working -->
<component v-bind:is="component_type(question.type)" :propData="question" />
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, toRefs } from 'vue';
import SelectInput from "./SelectInput";
import TextareaInput from "./TextareaInput";
const props = defineProps({
question: Object,
});
const components = {
'SelectInput': SelectInput,
'TextareaInput': TextareaInput
};
const component_type = (type) => components[type];
const { question } = toRefs(props);
</script>
Not too sure if this is the correct way of doing this but it now renders the correct component.

Call a function from another component using composition API

Below is a code for a header and a body (different components). How do you call the continue function of the component 2 and pass a parameter when you are inside component 1, using composition API way...
Component 2:
export default {
setup() {
const continue = (parameter1) => {
// do stuff here
}
return {
continue
}
}
}
One way to solve this is to use events for parent-to-child communication, combined with template refs, from which the child method can be directly called.
In ComponentB.vue, emit an event (e.g., named continue-event) that the parent can listen to. We use a button-click to trigger the event for this example:
<!-- ComponentB.vue -->
<script>
export default {
emits: ['continue-event'],
}
</script>
<template>
<h2>Component B</h2>
<button #click="$emit('continue-event', 'hi')">Trigger continue event</button>
</template>
In the parent, use a template ref on ComponentA.vue to get a reference to it in JavaScript, and create a function (e.g., named myCompContinue) to call the child component's continueFn directly.
<!-- Parent.vue -->
<script>
import { ref } from 'vue'
export default {
⋮
setup() {
const myComp = ref()
const myCompContinue = () => myComp.value.continueFn('hello from B')
return {
myComp,
myCompContinue,
}
},
}
</script>
<template>
<ComponentA ref="myComp" />
⋮
</template>
To link the two components in the parent, use the v-on directive (or # shorthand) to set myCompContinue as the event handler for ComponentB.vue's continue-event, emitted in step 1:
<template>
⋮
<ComponentB #continue-event="myCompContinue" />
</template>
demo
Note: Components written with the Options API (as you are using in the question) by default have their methods and props exposed via template refs, but this is not true for components written with <script setup>. In that case, defineExpose would be needed to expose the desired methods.
It seems like composition API makes everything a lot harder to do with basically no or little benefit. I've recently been porting my app to composition API and it required complete re-architecture, loads of new code and complexity. I really don't get it, seems just like a massive waste of time. Does anyone really think this direction is good ?
Here is how I solved it with script setup syntax:
Parent:
<script setup>
import { ref } from 'vue';
const childComponent = ref(null);
const onSave = () => {
childComponent.value.saveThing();
};
</script>
<template>
<div>
<ChildComponent ref="childComponent" />
<SomeOtherComponent
#save-thing="onSave"
/>
</div>
</template>
ChildComponent:
<script setup>
const saveThing = () => {
// do stuff
};
defineExpose({
saveThing,
});
</script>
It doesn't work without defineExpose. Besides that, the only trick is to create a ref on the component in which you are trying to call a function.
In the above code, it doesn't work to do #save-thing="childComponent.saveThing", and it appears the reason is that the ref is null when the component initially mounts.

parent component is not getting data from child in Nuxt app

This is driving me crazy so I hope that anyone can help.
I made a Nuxt app with #nuxt/content and I'm using Netlify-CMS to create content. That all seems to work fine. However I'm trying to display a component that contains a loop of the MD-files that I have, but in the index.vue nothing of the loop is displayed.
I know (a little) about props and $emit, but as I am not triggering an event this dosen't seem to work.
Component code:
<template>
<section>
<h1>Releases</h1>
<li v-for="release of rfhreleases" :key="release.slug">
<h2>{{ release.artist }}</h2>
</li>
</section>
</template>
<script>
export default {
components: {},
async asyncData({ $content, params }) {
const rfhreleases = await $content('releases', params.slug)
.only(['artist'])
.sortBy('createdAt', 'asc')
.fetch()
return {
rfhreleases,
}
},
}
</script>
And index.vue code:
<template>
<div>
<Hero />
<Releases />
<About />
<Contact />
</div>
</template>
<script>
export default {
head() {
return {
script: [
{ src: 'https://identity.netlify.com/v1/netlify-identity-widget.js' },
],
}
},
}
</script>
If I place my component code as part of index.vue, everything work, but I would love to avoid that and thats why I'm trying to place the loop in a component.
As stated on the Nuxt documentation:
This hook can only be placed on page components.
That means asyncData only works on components under pages/ folder.
You have several options:
You use fetch instead. It's the other asynchronous hook but it's called from any component. It won't block the rendering as with asyncData so the component it will instanciated with empty data first.
You fetch your data from the page with asyncData and you pass the result as a prop to your component
<template>
<div>
<Hero />
<Releases :releases="rfhreleases" />
<About />
<Contact />
</div>
</template>
<script>
export default {
async asyncData({ $content, params }) {
const rfhreleases = await $content('releases', params.slug)
.only(['artist'])
.sortBy('createdAt', 'asc')
.fetch()
return {
rfhreleases,
}
},
}
</script>

How do I call the API with Axios on the (child) component and present the result on the Page (parent) component in Nuxt?

If I run this code on the Page component (mountains.vue) it works and I get the data from the API with help with Axios:
<template>
<div>
<ul>
<li v-for="(mountain, index) in mountains" :key="index">
{{ mountain.title }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
mountains: [],
};
},
async asyncData({ $axios }) {
const mountains = await $axios.$get("https://api.nuxtjs.dev/mountains");
return { mountains };
},
};
</script>
But I want to put this code in a component (MountainsList) and do the Axios call in the component (MountainsList), but display the data on the Page component (mountains.vue) by injecting the component in Nuxt like this:
<template>
<MountainsList />
</template>
Now when I run the code, the data using Axios doesn't appear anymore... So how do I inject the data to the Page component above doing the Axios call in the child component?
asyncData only works on a page
From the docs
asyncData is called every time before loading the page component
One way you can accomplish what you want is passing the mountains in as a prop to the MountainList component. Something like below...
<template>
<MountainList :mountains="mountains" />
</template>
<script>
export default {
async asyncData({ $axios }) {
const mountains = await $axios.$get("https://api.nuxtjs.dev/mountains");
return { mountains };
},
};
</script>
And the component with the code and prop mountains...
<template>
<div>
<ul>
<li v-for="(mountain, index) of mountains" :key="index">
{{ mountain.title }}
</li>
</ul>
</div>
</template>
<script>
export default {
props: ['mountains'],
};
</script>
If you really want to make the API call in the child component you can use the fetch method.
Also you should not define a data() property on the page. I believe it will overwrite the server rendered data.
According to official docs :
Components in this directory will not have access to asyncData.
It means that any components inside the components folder cannot access that method.