How to display object in a variable in Vue 3 script setup - vue.js

I can't display object in Vue 3 script setup. I used ref, reactive and standard variables but all scenarios is unsuccessful.
I want to reflect the response from the getDetail request to the screen. getDetail is fetching this data asynchronously. I run into a problem in every scenario.
ref() usage
<script setup>
let movie = ref([])
const getMovieData = async ()=> {
try {
const data = await getDetail('movie', route.params.id)
movie.value.push(data)
}
catch (e){
console.log(e)
}
}
getMovieData()
</script>
<template>
<h1>{{movie[0].original_title}}</h1>
</template>
I am able to show data in this usage but I am getting these errors
reactive() usage
<script setup>
let movie = reactive({
data:null
})
const getMovieData = async ()=>{
try {
const data = await getDetail('movie', route.params.id)
movie.data = data
}catch (e){
console.log(e)
}
}
getMovieData()
</script>
<template>
<h1>{{movie.data.original_title}}</h1>
</template>
In this usage I can show data but I get similar errors
Standart variable usage
<script setup>
let movie
const getMovieData = async ()=>{
try {
const data = await getDetail('movie', route.params.id)
movie =data
}catch (e){
console.log(e)
}
}
getMovieData()
</script>
<template>
<h1>{{movie.id}}</h1>
</template>
In this usage, the data does not appear on the screen, because the movie object is formed asynchronously.
How do I manage to display this object on the screen in the most correct way without getting an error?

Write a v-if <h1 v-if="movie && movie.length">{{movie[0].id}</h1> So it only renders when data is available.

Template code runs immediately on component creation. In each case, before movie has been asynchronously assigned, your template code is trying to access some non-existent property. optional chaining is arguably the easiest way to prevent these types of errors:
movie[0]?.original_title
Another option is to provide a default value that matches your template usage so it doesn't error out:
let movie = ref([
{
original_title: ''
}
])

Related

Nested useFetch in Nuxt 3

How do you accomplish nested fetching in Nuxt 3?
I have two API's. The second API has to be triggered based on a value returned in the first API.
I tried the code snippet below, but it does not work, since page.Id is null at the time it is called. And I know that the first API return valid data. So I guess the second API is triggered before the result is back from the first API.
<script setup>
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs } = await useFetch(`/api/page/${page.Id}/paragraphs`)
</script>
Obviously this is a simple attempt, since there is no check if the first API actually return any data. And it is not even waiting for a response.
In Nuxt2 I would have placed the second API call inside .then() but with this new Composition API setup i'm a bit clueless.
You could watch the page then run the API call when the page is available, you should paragraphs as a ref then assign the destructed data to it :
<script setup>
const paragraphs = ref()
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
watch(page, (newPage)=>{
if (newPage.Id) {
useFetch(`/api/page/${newPage.Id}/paragraphs`).then((response)=>{
paragraphs.value = response.data
})
}
}, {
deep: true,
immediate:true
})
</script>
One solution is to avoid using await. Also, use references to hold the values. This will allow your UI and other logic to be reactive.
<script setup>
const route = useRoute()
const page = ref()
const paragraphs = ref()
useFetch(`/api/page/${route.params.slug}`).then(it=> {
page.value = it
useFetch(`/api/page/${page.value.Id}/paragraphs`).then(it2=> {
paragraphs.value = it2
}
}
</script>
You can set your 2nd useFetch to not immediately execute until the first one has value:
<script setup>
const route = useRoute()
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs } = await useFetch(`/api/page/${page.value?.Id}/paragraphs`, {
// prevent the request from firing immediately
immediate: false,
// watch reactive sources to auto-refresh
watch: [page]
})
</script>
You can also omit the watch option there and manually execute the 2nd useFetch.
But for it to get the updates, pass a function that returns a url instead:
const { data: page } = await useFetch(`/api/page/${route.params.slug}`)
const { data: paragraphs, execute } = await useFetch(() => `/api/page/${page.value?.Id}/paragraphs`, {
immediate: false,
})
watch(page, (val) => {
if (val.Id === 69) {
execute()
}
})
You should never call composables inside hooks.
More useFetch options can be seen here.

How to fire an $emit event from Vue Composable

I have a vue composable that needs to fire an event. I naively set it up as follows:
*// composable.js*
import { defineEmits } from "vue";
export default function useComposable() {
// Vars
let buffer = [];
let lastKeyTime = Date.now();
const emit = defineEmits(["updateState"]);
document.addEventListener("keydown", (e) => {
// code
emit("updateState", data);
}
// *App.vue*
<template>
<uses-composables
v-show="wirtleState.newGame"
#updateState="initVars"
></uses-composables>
</template>
<script setup>
const initVars = (data) => {
//code here
}
// usesComposable.vue
<template>
<button #click="resetBoard" class="reset-button">Play Again</button>
</template>
<script setup>
import { defineEmits } from "vue";
import useEasterEgg from "#/components/modules/wirdle_helpers/useEasterEgg.js";
useEasterEgg();
</script>
The error I get is "Uncaught TypeError: emit is not a function useEasterEgg.js:30:11
So obviously you can not use defineEmits in a .js file. I dont see anywhere in Vue docs where they specifically use this scenario. I dont see any other way to do this but using $emits but that is invoked in a template which my composable does not have. Any enlightenment much appreciated.
You can emit events from a composable, but it will need to know where the events should be fired from using context which can be accessed from the second prop passed to the setup function: https://vuejs.org/api/composition-api-setup.html#setup-context
Composable:
export default function useComposable(context) {
context.emit("some-event")
}
Component script:
<script>
import useComposable from "./useComposable"
export default {
emits: ["some-event"],
setup(props, context) {
useComposable(context)
}
}
</script>
To use it in a script setup, the best way I found was to declare the defineEmit first, and assigning it to a const, and pass it as a param to your composable :
const emit = defineEmit(['example']
useMyComposable(emit);
function useMyComposable(emit){
emit('example')
}
You can't access emit this way, as the doc says : defineProps and defineEmits are compiler macros only usable inside script setup. https://vuejs.org/api/sfc-script-setup.html
I'm not entirely sure of what you are trying to achieve but you can use vue-use composable library to listen to key strokes https://vueuse.org/core/onkeystroke/
Lx4
This question is a bit confusing. What is <uses-composable>? A composable generally is plan js/ts, with no template. If it had a template, it would just be a component. Even if it was a component, which I mean you could turn it into if thats what you wanted, I don't see anything there that suggests that would be a good idea.
So I guess the question is, why would you want to emit an event? If I'm following what you want, you can just do:
// inside useEasterEgg.js
document.addEventListener('keydown', () => {
// other code
const data = 'test';
updateStateCallback(data);
});
function useEasterEgg() {
function onUpdateState(callback) {
updateStateCallback = callback;
}
return {
onUpdateState,
}
}
// Put this wherever you want to listen to the event
const { onUpdateState } = useEasterEgg();
onUpdateState(data => console.log(data));
https://jsfiddle.net/tdfu3em1/3/
Alternatively, if you just want access to data, why not make it a ref and just use it where you want:
const data = ref();
document.addEventListener('keydown', () => {
// other code
data.value = resultOfOtherCode;
});
function useEasterEgg() {
return {
data,
}
}
// Anywhere you want to use it
const { data } = useEasterEgg();

vue 3 composition api, passing data and making it reactive

In my component I have a simple select menu with two options ("all", and "Investment"). The idea here is to get an array of data from a composable, and display on screen each row of this data. If I select "all" in the menu it displays all rows, if I select "Investment" it will filter the data and display only those with obj.link == "usa".
Once I fetch the data and bring it into my component, if I console.log the data, it works fine. If I console.log the data after i filter it, I get an empty array.
I have then tried to hard code the data in my component and test the filter function, and it works fine. So the error comes from how I am getting my data and how I try to use it. I have tried to use different hooks such as onMounted, but was unsuccessfull.
Here is a minimalistic sample of my code.
Any suggestion or advice is more than welcome
The composable that fetches the data from my database looks like this:
import {ref} from 'vue'
import { projectFirestore } from '../firebase/config'
import { collection, getDocs } from "firebase/firestore";
const getActorDocs = () => {
const actorDocs = []
const error = ref(null)
const loadActors = async () => {
try {
const querySnapshot = await getDocs(collection(projectFirestore, "actors"));
querySnapshot.docs.map(doc => {
actorDocs.push(doc.data())
})
} catch (err) {
error.value = err.message
console.log(error.value)
}
}
return { actorDocs, error, loadActors}
}
export default getActorDocs
My component:
<template>
<div class="col-2">
<span class="lbl">MA</span>
<select v-model="selectedMA" class="form-select" >
<option value="all">all</option>
<option value="Investment">Investment</option>
</select>
</div>
<p v-for="obj in actorListTest2" :key="obj" :value="obj"> {{obj}} </p>
<template/>
<script >
import {onMounted, onBeforeMount, ref} from 'vue'
import getActorDocs from '../../composables/getActorDocs'
export default {
setup(){
const selectedMA = ref("Investment")
const error = ref(null)
const {actorDocs, loadActors} = getActorDocs()
var actorListTest1 = actorDocs
const actorListTest2 = ref([])
loadActors() // loads actors array into actorDocs
actorListTest2.value = actorListTest1
console.log(actorListTest1) // <----- prints correctly (see image below)
if(selectedMA.value === "all"){
actorListTest2.value = actorListTest1
}else{
actorListTest2.value = actorListTest1.filter(obj => {
return obj.link == selectedMA.value
})
}
console.log(actorListTest2.value) // <----- prints undefined !
return { error, selectedMA, actorListTest2}
}//setup
}
</script>
This is the output of console.log(actorListTest1):
Then this is the output of console.log(actorListTest2) after filtering :
This is a known problem with console.log, it shouldn't be used to debug object values in real time.
actorDocs is not reactive and won't work correctly with asynchronous operations in Vue. Side effects are supposed to be done in lifecycle hooks, e.g.: mounted.
In current state getActorDocs isn't ready to be used with composition API because it's limited to follow promise control flow in order to avoid this race condition:
onMounted(async () => {
await loadActors();
console.log(actorListTest2.value);
});
A correct way to avoid this is to make actorDocs reactive array or a ref:
const actorDocs = reactive([]);
In case there's a need to access filtered value in side effect, e.g. console.log, this is done in a watcher
const actorListTest2 = computed(() => actorDocs.filter(...));
watch(actorListTest2, v => console.log(v));
onMounted(() => {
loadActors();
});

Transfer Data From One Component to Another

I have a component which makes a call to my backend API. This then provides me with data that I use for the component. I now want to create another component which also uses that data. While I could just do another api call that seems wasteful.
So, in Profile.vue i have this in the created() function.
<script>
import axios from 'axios';
import { bus } from '../main';
export default {
name: 'Profile',
data() {
return {
loading: false,
error: null,
profileData: null,
getImageUrl: function(id) {
return `http://ddragon.leagueoflegends.com/cdn/9.16.1/img/profileicon/` + id + `.png`;
}
}
},
beforeCreate() {
//Add OR Remove classes and images etc..
},
async created() {
//Once page is loaded do this
this.loading = true;
try {
const response = await axios.get(`/api/profile/${this.$route.params.platform}/${this.$route.params.name}`);
this.profileData = response.data;
this.loading = false;
bus.$emit('profileData', this.profileData)
} catch (error) {
this.loading = false;
this.error = error.response.data.message;
}
}
};
</script>
I then have another child component that I've hooked up using the Vue router, this is to display further information.
MatchHistory compontent
<template>
<section>
<h1>{{profileDatas.profileDatas}}</h1>
</section>
</template>
<script>
import { bus } from '../main';
export default {
name: 'MatchHistory',
data() {
return {
profileDatas: null
}
},
beforeCreate() {
//Add OR Remove classes and images etc..
},
async created() {
bus.$on('profileData', obj => {
this.profileDatas = obj;
});
}
};
</script>
So, I want to take the info and display the data that I have transferred across.
My assumption is based on the fact that these components are defined for two separate routes and an event bus may not work for your situation based on the design of your application. There are several ways to solve this. Two of them listed below.
Vuex (for Vue state management)
Any local storage option - LocalStorage/SessionStorage/IndexDB e.t.c
for more information on VueX, visit https://vuex.vuejs.org/.
for more information on Localstorage, visit https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage.
for more information on session storage, visit https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage
The flow is pretty much the same for any of the options.
Get your data from an API using axios as you did above in Profile.vue
Store the retrieved data with VueX or Local/Session storage
Retrieve the data from Vuex or local/session storage in the created method of MatchHistory.vue component
For the local / session storage options, you will have to convert your object to a json string as only strings can be stored in storage. see below.
in Profile.vue (created)
const response = await axios.get(........)
if(response){
localStorage.setItem('yourstoragekey', JSON.stringify(response));
}
In MatchHistory.Vue (created)
async created() {
var profileData = localStorage.getItem('yourstoragekey')
if(profileData){
profileData = JSON.parse(profileData );
this.profileData = profileData
}
}
You can use vm.$emit to create an Eventbus
// split instance
const EventBus = new Vue({})
class IApp extends Vue {}
IApp.mixin({
beforeCreate: function(){
this.EventBus = EventBus
}
})
const App = new IApp({
created(){
this.EventBus.$on('from-mounted', console.log)
},
mounted(){
this.EventBus.$emit('from-mounted', 'Its a me! Mounted')
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
further readings
You can make use of the VUEX which is a state management system for Vue.
When you make api call and get the data you need, you can COMMIT a MUTATION and pass your data to it. What it will do, it will update your STATE and all of your components will have access to its state (data)
In your async created(), when you get response, just commit mutation to your store in order to update the state. (omitted example here as the vuex store will need configuration before it can perform mutations)
Then in your child component,
data(){
return {
profileDatas: null
}
},
async created() {
this.profileDatas = $store.state.myData;
}
It might seem like an overkill in your case, but this approach is highly beneficial when working with external data that needs to be shared across multiple components

Prefetching data with Nuxt to improve loading time

As far as I understand people are using server side rendering (ssr) to improve user experience and SEO with fast and content ready pages.
Recently I've started project with vue+nuxt in ssr mode.
What I've noticed is that when I try to prefetch data in nuxtServerInit action I have to wait for the async call to finish before page could be served to me.
As far as I know it will only hurt SEO and user experience.
export const actions = {
async nuxtServerInit ({ commit }) {
const response = await this.$axios.$get('games')
commit('setGameList', response.data)
}
}
Is there a way to actually prefetch data once and cache it for some period of time so that users would not be forced to wait?
Also what is the good usecase for nuxtServerInit? Cant understand the purpose of it..
Use The fetch Method
<template>
<h1>Stars: {{ $store.state.stars }}</h1>
</template>
<script>
export default {
async fetch ({ store, params }) {
let { data } = await axios.get('http://my-api/stars')
store.commit('setStars', data)
}
}
</script>
Remember to use Vuex to work well!
UPDATE: How to share the fetch function
1 - Create a file with the function:
// defaultFetch.js
module.exports = async function defaultFetch({ store, params }){
// Put some developer magic here to make the code works for you
let { data } = await axios.get('http://my-api/stars');
store.commit('setStars', data);
}
2 - Import and use in other components
// amazingComoponent1.vue
<template>
<h1>Stars: {{ $store.state.stars }}</h1>
</template>
<script>
import defaultFetch from "../utils/defaultFetch";
export default {
fetch: defaultFetch
}
</script>
// amazingComoponent2.vue
<template>
<h1>Stars: {{ $store.state.stars }}</h1>
</template>
<script>
import fetch from "../utils/defaultFetch";
export default {
fetch,
}
</script>
UPDATE 2: How to use and configure axios intance
Very easy, update the defaultFetch.js:
// defaultFetch.js
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
module.exports = async function defaultFetch({ store, params }){
// Put some developer magic here to make the code works for you
let { data } = await instance.get('http://my-api/stars');
store.commit('setStars', data);
}
Hope helps :)