Data from store not reactive in Vue.js 3 - javascript

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);
});
}
}
});

Related

Vuex/Redux store pattern - sharing single source of data in parent and child components that require variations of that data

I understand the benefits of using a store pattern and having a single source of truth for data shared across components in an application, and making API calls in a store action that gets called by components rather than making separate requests in every component that requires the data.
It's my understanding that if this data needs to change in some way, depending on the component using the data, this data can be updated by calling a store action with the appropriate filters/args, and updating the global store var accordingly.
However, I am struggling to understand how to solve the issue whereby a parent component requires one version of this data, and a child of that component requires another.
Consider the following example:
In an API, there exists a GET method on an endpoint to return all people. A flag can be passed to return people who are off sick:
GET: api/people returns ['John Smith', 'Joe Bloggs', 'Jane Doe']
GET: api/people?isOffSick=true returns ['Jane Doe']
A parent component in the front end application requires the unfiltered data, but a child component requires the filtered data. For arguments sake, the API does not return the isOffSick boolean in the response, so 2 separate requests need to be made.
Consider the following example in Vue.js:
// store.js
export const store = createStore({
state: {
people: []
},
actions: {
fetchPeople(filters) {
// ...
const res = api.get('/people' + queryString);
commit('setPeople', res.data);
}
},
mutations: {
setPeople(state, people) {
state.people = people;
}
}
});
// parent.vue - requires ALL people (NO filters/args passed to API)
export default {
mounted() {
this.setPeople();
},
computed: {
...mapState([
'people'
])
},
methods: {
...mapActions(['setPeople']),
}
}
// child.vue - requires only people who are off sick (filters/args passed to API)
export default {
mounted() {
this.setPeople({ isOffSick: true });
},
computed: {
...mapState([
'people'
])
},
methods: {
...mapActions(['setPeople']),
}
}
The parent component sets the store var with the data it requires, and then the child overwrites that store var with the data it requires.
Obviously the shared store var is not compatible with both components.
What is the preferred solution to this problem for a store pattern? Storing separate state inside the child component seems to violate the single source of truth for the data, which is partly the reason for using a store pattern in the first place.
Edit:
My question is pertaining to the architecture of the store pattern, rather than asking for a solution to this specific example. I appreciate that the API response in this example does not provide enough information to filter the global store of people, i.e. using a getter, for use in the child component.
What I am asking is: where is an appropriate place to store this second set of people if I wanted to stay true to a store focused design pattern?
It seems wrong somehow to create another store variable to hold the data just for the child component, yet it also seems counter-intuitive to store the second set of data in the child component's state, as that would not be in line with a store pattern approach and keeping components "dumb".
If there were numerous places that required variations on the people data that could only be created by a separate API call, there would either be a) lots of store variables for each "variation" of the data, or b) separate API calls and state in each of these components.
Thanks to tao I've found what I'm looking for:
The best approach would be to return the isOffSick property in the API response, then filtering the single list of people (e.g. using a store getter), thus having a single source of truth for all people in the store and preventing the need for another API request.
If that was not possible, it would make sense to add a secondary store variable for isOffSick people, to be consumed by the child component.

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.

How to share variables between functions in Vue

I'm learning Vue and trying to test out how I'd pass a user input from one function to another.
My use case is this. I want to take a users input, store it and then supply that to a second function as a variable that it can use to make an API call.
In isolation the two code sections are:
User Input
<div id="v-model-basic" class="demo">
<input v-model="message" placeholder="edit me" />
<p>Message is: {{ message }}</p>
</div>
Vue.createApp({
data() {
return {
message: ''
}
}
}).mount('#v-model-basic')
API Call
import axios from 'axios';
/* API call for the weather data */
export default {
name: "Weather",
data() {
return {
weatherList: []
};
},
methods: {
getweatherData() {
axios.get("https://anAPIpath.com/that-I-want-to-add-user-input-to").then(response => (this.weatherList = response.data));
}
}
};
Is there a way to do this in Vue where they are both methods within that export function, or is that just the wrong approach? As mentioned, new and learning Vue and I want to ensure I approach this the "Vue way" so things are as neat and tidy as possible.
Vue has several options for passing variables between components.
props
You can make the variable a prop of the other:
props: {
message: String
}
The prop is accessed with this.message but it is recommended you copy it to a variable in data and reflect changes with $emit (see documentation link, and $emit section below).
See documentation: https://v3.vuejs.org/guide/component-props.html#props
$emit
You can let a parent component know about the other component's changes with $emit. You can just name an event yourself and pass the value as the first parameter.
this.$emit('messageUpdate', this.message);
See documentation: https://v3-migration.vuejs.org/breaking-changes/emits-option.html#emits-option
VueX
With the VueX data storage you can transfer variables from any component across your app to any other.
This takes a little more finess to set up. I suggest going with the above first.
If you are interested (definitely worth to learn), here is the link:
https://vuex.vuejs.org/guide/#getting-started
I see you are doing well, you just need to add the API call to a script tag in the same file and then access the input message like this:
axios.get("https://anAPIpath.com/"+ this.message)

Vue js, how do I dynamically update content based on the url parameters?

I'm trying to dynamically update the content using url parameters. So I'm basically reusing the view to render content based on the url.
while that works fine for the initial load, it won't update when I'm trying to access a different link using the same view.
I've to reload the page in order for the content to update based on the new url params.
I read things and examples about using a watcher to achieve what I need, however all the examples used custom methods which I'm not interested in,
so I'm not sure how to restracture my code to work with watchers
My script looks as follows
<script>
import axios from "axios";
export default {
data() {
return {
items: [],
};
},
async created() {
const response = await axios.get("http://localhost/test/records/"+this.$route.params.dates+"/"+this.$route.params.datee);
this.items = response.data.data;
},
};
</script>
the params this.$route.params.dates and this.$route.params.datee are dates that allow me to fetch data between these two values.

How can I correctly lazy load a json file in Vue.js to reduce bundle size?

I am using Vue.js and Laravel for my project and recently added two animations using a plugin named Lottie. Each animation is a component, and they both use an individual JSON file to animate a group of PNG images (similar to a PNG sequence). These two JSON files are stored locally in the project folder under the path /public/data/.
Firstly the JSON files are not being read unless I put in the absolute path (/users/username/documents/projectname/public/data/filename.json), is there no way I can get this to work just by using /data/filename.json?
Secondly, when I add the code below in my component, my JS files are compiled to separate chunks as expected:
const animationData = () =>
import("/users/username/documents/projectname/public/data/filename.json");
I get the following error when the animation tries to run:
Invalid prop: type check failed for prop "data". Expected Object, got Function
found in
---> <VueLottie>
However when I import my json file using a normal import in my component like below it works fine and shows the animation:
import animationData from "/users/username/documents/projectname/public/data/filename.json";
My animation components are both set up like this:
<template>
<vue-lottie ref="lottie" loop autoplay :data="animationData" :height="400" :width="400"></vue-lottie>
</template>
<script>
import vueLottie from "vue-lottie-ssr";
import animationData from '/users/username/documents/projectname/public/data/filename.json'
export default {
name: 'animation',
components: {
vueLottie
},
data () {
return {
speed: 1,
animationData
}
},
computed: {
lottie () {
return this.$refs.lottie
}
}
}
</script>
I have also tried getting the JSON file via an axios call when the component mounts, but the same error occurs.
Update
I updated my code so that each component is lazy loaded instead of the JSON file. Like so:
components: {
WinAnimation: () => import("./WinAnimation.vue");
LoseAnimation: () => import("./LoseAnimation.vue");
}
However now I'm getting the following error:
Unknown custom element: <win-animation> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Update 2
I realised why I was getting an error message. The correct way was to add the following at the top of my script inside the parent vue file.
const winAnimation = () => import("./WinAnimation.vue");
const loseAnimation = () => import("./LoseAnimation.vue");
and then inside export default {...} I forgot to add the names, so:
components: { winAnimation, loseAnimation }
Now my code has been split and my app.js file size has reduced by almost a half! :)
1st - don't use vue-lottie library. If you take a look at the source code, the main and only thing which should be provided by this library is component src/lottie.vue (+ it's dependency lottie-web) but for some reason, NPM package also contains whole demo app including the demo JSON file (src/assets/pinjump.json)
If you take a look at lottie.vue component, its just very little and very simple wrapper for lottie-web which provides main functionality. By getting rid of vue-lottie you will get following benefits:
vue-lottie completely ignores one of the lottie-web options which is using path instead of animationData - documentation is not very clear here but I would guess that by providing path, the library will try download the animation data ad-hoc so you don't need to include it in your bundle. Worth trying imho...
Loading animation data on demand
why are you using dynamic import on JSON file instead of dynamically importing whole component ? By making separate chunk on component level, dynamic chunk will include not only your json data but also lottie-web which is also not small. And Vue will handle loading of the component without any additional code changes...
if you still want to load on demand only your JSON data, you must understand that Webpack dynamic import (import(".....")) is returning Promise and lottie-web (and in turn vue-lottie) is expecting object. So you must do something like this:
<script>
import lottie from 'lottie-web';
const animationData = () =>
import("/users/username/documents/projectname/public/data/filename.json");
export default {
mounted () {
animationData().then(function(data) {
this.anim = lottie.loadAnimation({
// other options
animationData: data
})
});
}
}
</script>
Update
You should be always very careful when considering adding 3rd party components into your project. One more thing I'v noticed is that lottie-web has destroy() method in it's API. This indicates that it is creating some resources (DOM elements probably) which needs to be cleaned up. This is something vue-lottie component is not handling at all and can lead to nasty memory leaks in your app. You can read about the problem here
When the animationData property is set it is a function, hence the line:
Expected Object, got Function
It needs an object, not a function.
The function being:
const animationData = () =>
import("/users/username/documents/projectname/public/data/filename.json");
When defining the animationData property you need to set an object as its value. Then when mounting fetch the data (or use Axios if you prefer that) to update the animationData property on the component.
N.B. I have never used Vue, so I hope that what I am saying is correct.
export default {
name: 'animation',
components: {
vueLottie
},
data () {
return {
speed: 1,
animationData: {}
}
},
computed: {
lottie () {
return this.$refs.lottie
}
},
mounted() {
fetch('/users/username/documents/projectname/public/data/filename.json')
.then(response => response.json())
.then(json => this.animationData = json;);
)
}
}

Categories

Resources