Component's data property is not reactive when fetched via fetch method - javascript

I have a simple "show" component that fetches some data via an API call and renders it on the page.
<template>
<div class="showNote">
<div>
{{note.title}}
</div>
</div>
</template>
<script>
import NotesAPI from '#/api/notes'
export default {
data() {
return {
note: {}
}
},
async fetch() {
let id = this.$route.params.id
let response = await NotesAPI.fetchNote(id)
let data = response.data
this.note.title = data.title
}
}
</script>
I'm finding that the rendered template is blank even though the data has been fetched and set on the data field. I can confirm this via the Vue Inspector
It seems like the data property is not reactive and I'd need some way to force the re-rendering of the page. But obviously this would be the wrong approach. What am I missing? Why is the component not rendering my data changes?

Try to use this.$set to set nested value of an object :
this.$set(this.note,'title', data.title)

Related

Possible/How to use a VueJS component multiple times but only execute its created/mounted function once?

I am trying to create a VueJS component that does the following: 1) download some data (a list of options) upon mounted/created; 2) display the downloaded data in Multiselct; 3) send selected data back to parent when user is done with selection. Something like the following:
<template>
<div>
<multiselect v-model="value" :options="options"></multiselect>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
export default {
components: { Multiselect },
mounted() {
this.getOptions();
},
methods:{
getOptions() {
// do ajax
// pass response to options
}
},
data () {
return {
value: null,
options: []
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
This is mostly straightforward if the component is only called once in a page. The problem is I may need to use this component multiple times in one page, sometimes probably 10s of times. I don't want the function to be called multiple times:
this.getOptions();
Is there a way to implement the component somehow so no matter how many times it is used in a page, the ajax call will only execute once?
Thanks in advance.
Update: I assume I can download the data in parent then pass it as prop if the component is going to be used multiple times, something like the following, but this defies the purpose of a component.
props: {
optionsPassedByParents: Array
},
mounted() {
if(this.optionsPassedByParents.length == 0)
this.getOptions();
else
this.options = this.optionsPassedByParents;
},
The simple answer to your question is: you need a single place in charge of getting the data. And that place can't be the component using the data, since you have multiple instances of it.
The simplest solution is to place the contents of getOptions() in App.vue's mounted() and provide the returned data to your component through any of these:
a state management plugin (vue team's recommendation: pinia)
props
provide/inject
a reactive object (export const store = reactive({/* data here */})) placed in its own file, imported (e.g: import { store } from 'path/to/store') in both App.vue (which would populate it when request returns) and multiselect component, which would read from it.
If you don't want to request the data unless one of the consumer components has been mounted, you should use a dedicated controller for this data. Typically, this controller is called a store (in fairness, it should be called storage):
multiselect calls an action on the store, requesting the data
the action only makes the request if the data is not present on the store's state (and if the store isn't currently loading the data)
additionally, the action might have a forceFetch param which allows re-fetching (even when the data is present in state)
Here's an example using pinia (the official state management solution for Vue). I strongly recommend going this route.
And here's an example using a reactive() object as store.
I know it's tempting to make your own store but, in my estimation, it's not worth it. You wouldn't consider writing your own Vue, would you?
const { createApp, reactive, onMounted, computed } = Vue;
const store = reactive({
posts: [],
isLoading: false,
fetch(forceFetch = false) {
if (forceFetch || !(store.posts.length || store.isLoading)) {
store.isLoading = true;
try {
fetch("https://jsonplaceholder.typicode.com/posts")
.then((r) => r.json())
.then((data) => (store.posts = data))
.then(() => (store.isLoading = false));
} catch (err) {
store.isLoading = false;
}
}
},
});
app = createApp();
app.component("Posts", {
setup() {
onMounted(() => store.fetch());
return {
posts: computed(() => store.posts),
};
},
template: `<div>Posts: {{ posts.length }}</div>`,
});
app.mount("#app");
<script src="https://unpkg.com/vue/dist/vue.global.prod.js"></script>
<div id="app">
<Posts v-for="n in 10" :key="n" />
</div>
As you can see in network tab, in both examples data is requested only once, although I'm mounting 10 instances of the component requesting the data. If you don't mount the component, the request is not made.

Data from store not reactive in Vue.js 3

This is my first attempt at building a web app with Vuejs. I've been trying to get data from an external JSON API and display it on my app. The JSON fetch etc is working fine. but I can't get the data to be displayed reactively on my component.
As you can read in Appraisal.js given an API link some data is populated in Appraisal.app_data. The data always has an array called items (that's just how the API is. I'll add validation later). As a proof of concept I'm trying to display the number of elements in the items array.
Since other components in my app will also use this data, I'm using an external store as the data source everywhere. One of the components calls Appraisal.setLink() on getting some user input. That part is working as expected. However the DOM contents don't change at all.
I referred to State Management for setting up the external store. I also referred to some other answers on StackOverflow with a similar issue and got the following suggestions:
The data should be initialized to undefined or null instead of {} for reactivity to work.
Properties of objects are not reactive. But by my understanding this was changed in Vue3 where it doesn't matter because proxies are in use. Either way I tried using the Object.assign({}, ..., ...) method but it did not help.
Arrow functions cannot be used in methods for reactive objects. If I remove the arrow function and put the body inside .then(function(data) {...}) it complains that this is not defined for the second then function on fetch
// --- src/components/AppraisalView.vue
<script setup>
import ItemView from './ItemView.vue';
</script>
<template>
<div v-if="app_data">{{app_data.items.length}} items in appraisal</div>
<div v-else>Enter link to get quote</div>
</template>
<script>
import {Appraisal} from '../stores/Appraisal.js';
export default {
data() {
return {
app_data: Appraisal.app_data,
}
},
}
</script>
// ---- src/store/Appraisal.js
import {reactive} from 'vue'
import {BuybackCalculator} from './BuybackCalculator.js';
export const Appraisal = reactive({
link: '',
app_data: undefined,
methods: {
setLink(value) {
if (this.link == value) return;
this.link = value;
console.log('Updating appraisal with: '+this.link);
fetch(this.link+".json")
.then((response) => response.json())
.then((data) => {
this.app_data = data;
console.log(this.app_data);
BuybackCalculator.methods.calculate_buyback(this.app_data);
});
}
}
});

How to pass variable data between components in Vue js

Here I'm trying to pass the variable header_url data to my other vue pages. Since I also want to use this for push and post methods in the other vue script tags. I'm not sure how to do it? Since, I can only find examples with data been transferred between html tags.
// Home.vue
<template>
<div>
Home
<h2>logout</h2>
<a v-bind:href="url">logout</a>
</div>
</template>
<script>
export default {
data: () => ({
url:"https:..."
})
}
var header_url = window.location.href;
if (header_url.length>100) {
var token=window.location.href.match(/\#(?:id_token)\=([\S\s]*?)\&/)[1]; // eslint-disable-line
console.log(token);
}
</script>
To pass data between two URLs in single page app, you need use Vue Router
and Route Meta Fields.
From the docs :
Sometimes, you might want to attach arbitrary information to routes like transition names, who can access the route, etc. This can be achieved through the meta property which accepts an object of properties and can be accessed on the route location and navigation guards

Vue Router params not updating in page

Hello I have some conditional logic in my view based on the current page
in setup I have
const curVidType = ref(route.params.videoTopic);
I return it and then print out like
<h1>Welcome to {{ curVidType }} videos</h1>
However it only works if I refresh. If I browse to a new page it stays the old one even though the browser url changes. after refresh it updates. Thanks so much for any help
Try adding :key to your Component to make sure it updates when param changes:
<your-component :key="$route.params.videoTopic"></your-component>
You're initializing the variable as a const, which means it does it once and then doesn't set it. If you change curVidType to a computed value, you can have it react to changes in the router params.
computed: {
curVidType() {
return route.params.videoTopics
}
}
This will have the value of curVidType set to change when videoTopics does
EDIT:
Having read the comments and looked at the comments and read some of the documentations, ref() will create a reactive object but I'm not sure it's reacting to the original object. But it looks like the toRef() function can create that bridge.
const curVidType = toRef(route.params, 'videoTopics')
Should allow for curVidType.value to also reflect changes to to route.params.videoTopics
Would be nice if you could share some more code with us in order to help you.
This should just work fine
<template>
{{ curVidType }}
</template>
<script>
import { useRoute } from 'vue-router';
export default {
setup() {
const { params } = useRoute();
const curVidType = params.videoTopic
return {
curVidType,
};
},
};
</script>

Vue.js vuex props don't rerender

I'm using Vuex and display data like this:
<template>
<div>
<div>
<div v-for="port in ports">
<port :port="port"></port>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
computed: {
...mapState({
ports: state => state.ports.ports,
})
}
}
</script>
The problem that I have now is that when I change the ports state in another component it updates the ports state correctly but the port in the v-for loop is not updated.
How do I make sure the data in the port component rerenders correctly as well?
Vue Beginner Gotchas: Why isn't the DOM updating?
Most of the time, when you change a Vue instance’s data, the view updates. But there are two edge cases:
When you are adding a new property that wasn’t present when the data was observed.
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property.
If you are changing your ports data by doing something like this:
this.ports[0] = 1234;
Or something like this:
this.ports[4].property = 'some value';
Then Vue won't be able to detect that a change occurred. You can get around this pitfall by doing:
Vue.set(this.ports, 0, 1234);

Categories

Resources