Use fetch and asyncData together - vue.js

I've a doubt..
In my Nuxt static project pages (Nuxt version => 2.12), I need to be fast with data recuperation.
The assumption:
With asyncData, the objective of speed is checked, because with asyncData I can get data during the component render. But this project has 3 different possible API calls, as many as the languages that I set and, when I select one language the value is saved in the Vuex store.
At the same time of this process, the language value is also saved on local storage, so, if I use only asyncData, when the page is refreshed the API call will not be the right call with the saved language (asyncData can't access the local storage).
Here, the fetch hook enters into the game, with the watch set on the state language value of the store, it can trigger the fetch and get the right data. Moreover, even when the page is refreshed, the fetch hook, being able to read the values of the local storage, can do its work great.
So why don't I use only the fetch hook? Because the fetch is slower than asyncData.
The questions:
Is the use of both fetch and asyncData an anti-pattern?
Is there a better way?
Here my code:
export default {
asyncData (context) {
const slug = (context.route.path === '/' || context.route.path === '') ? '/home' : context.route.path
return context.app.$storyapi
.get(`cdn/stories${slug}`, {
language: context.store.state.language.language
}).then((res) => {
return res.data
}).catch((res) => {
context.$errorMessage(res.response,
'Sorry but this content doesn\'t extist', `Sorry, but the content called: "${context.route.name}" has a problem or doesn't exist`
)
})
},
data () {
return {
story: {
content: {}
}
}
},
async fetch () {
const slug = (this.$route.path === '/' || this.$route.path === '') ? '/home' : this.$route.path
const { data } = await this.$storyapi.get(`cdn/stories${slug}`, {
language: this.$store.state.language.language
})
this.story = data.story
},
watch: {
'$store.state.language.language': '$fetch'
}
}
Just to complete the information I would like to further say that the code works well, the matter is concerning the best practice.

Since when fetch() is slower than asyncData()?
Also, if you really need to re-run something you can totally use await this.$nuxt.refresh().
You could probably run both of those but they can kinda make the same thing, so it's kinda a duplication and I'd recommend to choose only one.
I'm not sure how it being located in localStorage is an issue but you could probably use one of the available packages to have an universal storage of this data, like this one: https://github.com/unjs/unstorage
If not, cookies should be enough too (available both on server and client).

Related

Property accessed during render but not defined in VUE

I receive an error when I load the page
My code looks like this :
<script>
methods: {
getPicture() {
var base = this;
axios
.get("http://localhost:3000/pictures/" + this.username)
.then(function (response) {
const { pictureData } = response.data;
base.studentImage = "data:image/jpg; base64," + pictureData;
console.log(base.studentImage);
});
},
}, //END OF METHOD
</script>
I want to display image in My front end. Where do I make a mistake ? Thank you in advance.
Welcome to Stack Overflow!
Sadly, I can't comment on this for more clarity, so my response has to be an answer.
This error occurs because the DOM and/or Vue Instance and/or Template uses the value "studentImage" before it is made available or defined incorrectly.
I recommend removing the variable "base" because Vue is very picky with its use of "this," so you usually wanna stay away from explicitly assigning "this" to a variable. As commented by Estus Flask below, this even applies to common JS. Check here for more information on referring to the correct "this" in JS.
Honestly, from what I can see with the code you've shared, that's likely the cause of your error. You'll want to define "studentImage" within your data object so Vue is aware of its existence.
Here is an example:
data: () => ({
studentImage: '',
}),
methods: {
getPicture() {
axios
.get("http://localhost:3000/pictures/" + this.username)
.then(function (response) {
const { pictureData } = response.data;
this.studentImage = "data:image/jpg; base64," + pictureData;
console.log(this.studentImage);
});
},
},
If it is not, then you'll want to make sure you're checking for "studentImage" before using it within the instance and or calling the method "getPicture," in the appropriate lifecycle.
I'm answering this assuming you're using the latest version of Vue, version three.
If this helps, please let me know!

How to stop Vue.js 3 watch() API triggering on exit

I have implemented a watch within a Vue component that displays product information. The watch watches the route object of vue-router for a ProductID param to change. When it changes, I want to go get the product details from the back-end API.
To watch the route, I do this in Product.vue:
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute();
async function getProduct(ProductID) {
await axios.get(`/api/product/${ProductID}`).then(..do something here)
}
// fetch the product information when params change
watch(() => route.params.ProductID, async (newID, oldID) => {
await getProduct(newId)
},
//watch options
{
deep: true,
immediate: true
}
)
},
}
The above code works, except that if a user navigates away from Product.vue, for example using the back button to go back to the homepage, the watch is triggered again and tries to make a call to the API using undefined as the ProductID (becaues ProductID param does not exist on the homepage route) e.g. http://localhost:8080/api/product/undefined. This causes an error to be thrown in the app.
Why does the watch trigger when a user has navigated away from Product.vue?
How can this be prevented properly? I can do it using if(newID) { await getProduct(newId) } but it seems counterintuitive to what the watch should be doing anyway.
UPDATE & SOLUTION
Place the following at the top replacing the name for whatever your route is called:
if (route.name !== "YourRouteName") {
return;
}
That will ensure nothing happens if you are not on the route you want to watch.
I ran into the same problem. Instead of watching the current route, use vue-router onBeforeRouteUpdate, which only gets called if the route changed and the same component is reused.
From https://next.router.vuejs.org/guide/advanced/composition-api.html#navigation-guards:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'
export default {
setup() {
// same as beforeRouteLeave option with no access to `this`
onBeforeRouteLeave((to, from) => {
const answer = window.confirm(
'Do you really want to leave? you have unsaved changes!'
)
// cancel the navigation and stay on the same page
if (!answer) return false
})
const userData = ref()
// same as beforeRouteUpdate option with no access to `this`
onBeforeRouteUpdate(async (to, from) => {
// only fetch the user if the id changed as maybe only the query or the hash changed
if (to.params.id !== from.params.id) {
userData.value = await fetchUser(to.params.id)
}
})
},
}
watch registers the watcher inside an vue-internal, but component-independent object. I think it's a Map. So destroying the component has no effect on the reactivity system.
Just ignore the case where newID is undefined, like you already did. But to prevent wrapping your code in a big if block just use if(newID === undefined)return; at the beginning of your callback. If your ids are always truthy (0 and "" are invalid ids) you can even use if(!newID)return;.
well, in your use case the best approach would be to have a method or function which makes the api call to the server, having watch is not a really good use of it, because it will trigger whenever route changes and you do not want that to happen, what you want is simply get the productID from route and make the api call,
so it can be done with getting the productID in the created or mounted and make the api call!

Setting data returned by useQuery as state

I have been using client.query to get data from my database and setting states using useState.
Here's an example:
const [videos, setVideos] = useState([]);
client.query({ query: GET_VIDEOS })
.then(response => {
setVideos(response.data.videos);
})
However, this does not load 100% of the time. Usually, it doesn't load when I load the app for the first time in a while. I typically have to reboot in these situations.
This issue makes me want to look into useQuery instead of client.query.
However, the examples in the documentation only show how we can use useQuery to make direct changes in the components.
Example:
function Dogs({ onDogSelected }) {
const { loading, error, data } = useQuery(GET_DOGS);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return (
<select name="dog" onChange={onDogSelected}>
{data.dogs.map(dog => (
<option key={dog.id} value={dog.breed}>
{dog.breed}
</option>
))}
</select>
);
}
However, I don't want to make changes to the components right now. I would much rather query the data and then set the queried data as a state, using useState.
How would I best accomplish this? One idea that I had was to create a component that returns null but queries the data. It would look something like this:
function Videos() {
const { loading, error, data } = useQuery(GET_VIDEOS);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
setVideos(data);
return null;
}
I was curious to hear what are best practices for such situations.
react-apollo's useQuery has an onCompleted as one of its options, so you can use it like
const { loading, error } = useQuery(GET_VIDEOS, {
onCompleted: (data) => setVideos(data)
});
the best practise would be to use the data directly, without setting the state.
you have only showed the calling of setState, somewhere in your component the video state variable is used right?
pass the fetched data directly there. you need not call "setVideos" to trigger the change. whenever the query result is changed the UI will be changed accordingly.
There is useLazyQuery if your use case is not to fetch the data upfront.

Watch for URL query parameter in Vuex store

I am using Nuxt.js with Vuex and I would like to trigger a mutation when somebody enters in my web with a certain parameter (ex: https://example.com/?param=abc), and pass the parameter to a state.
I tried to check the documentation of the watchQuery property https://nuxtjs.org/api/pages-watchquery, but there’s no examples about how to do this, I just found this How to watch on Route changes with Nuxt and asyncData but I can’t see any way of how to write an action in Vuex store with watchQuery.
I tried writing:
actions: {
watchQuery: true,
asyncData ({ query, app }) {
const { start } = query
const queryString = start ? `?start=${start}` : ''
return app.$axios.$get(`apps/${queryString}`)
.then(res => {
commit('setParam',res.data);
})
},
}
But that syntax is not allowed.
Any help would be welcome, thanks in advance!
From my understanding watchQuery sets a watcher for query string, meaning it's waiting for the query to change while the page is already rendered making it possible to call methods like asyncData() again.
Since you only want to save a certain parameter when the user enters the page and then pass the paramater to a state you just need to move your asyncData method to a page from which you want to get the parameter, you will also need to extract store and query from the context automatically passed into asyncData and then using the store and query save the query parameter into your state.
Here is a simple demonstrantion
// Your page from which you want to save the param
export default {
asyncData({store, query}) { // here we extract the store and query
store.state.somethingForSavingTheParam = query.nameOfTheParamYouWantToSave
// instead of using store.state you could use store.commit(...) if that's what you want
}
}

Using one vuex module store in multiple sibling components

I have one global state with some modules.
now i have vue components for various parts of my page.
i have everything setup so /foo uses the foo store (this works).
the created method loads data from an API and writes it to the store
now i have /foo/bar as another (sibling) component, but it needs to access the same store as /foo, but i can't get it to work.
if i enter /foo/bar/ in the URL, there is nothing in the store.
but if i switch to /foo, and then back to /foo/bar, the data is in the store and being output correctly
I've tried registering /foo/bar as a child, which seemed to have no effect (and actually it's not really a child, but just another page with the same data..)
I also tried
state: {
...mapState([
'foo'
)]
}
in /foo/bar, but that doesn't seem to be the right way either
what is the best practice to
load data from API on created on any of a specified set of pages
access said data on any of those pages (i.e. sharing the same store)
i've tried all day to find a solution, but it seems I didn't understand something.
thanks for your help :)
EDIT
actually, while i read my question again, i think my whole problem is the data not being loaded (because the created method is not called). how can i make sure this happens on any page using the store and just once? i can't just write an api call in every created method, can i?
Well, I think just to summarize your problem could be called like you're not being able to access the same state between two different componentes.
What I do normally is that I make an API call from one component inside the method beforeMount, that will guarantee that once my component is created, the data will be available to be used.
Furthermore, after calling the api, I update my state so after that I can call it from everywhere.
One thing that you have to take care with is which component is loaded first?
If A is B's parent, then you should load data inside A.
However, if A and B are siblings, then you should load data inside both of them because you can access first either Component A or B, then you don't know when the data is going to be available. In that case, I would load the data in both of the components.
Also, add cache to your server so you don't need to load the same data again.
For example:
State
{
data: {}
}
Component A
export default {
name: 'Batch',
beforeMount() {
this.getDataFromAPI();
},
methods: {
// getDataFromAPI will store its return inside data with a mutation
...mapActions(['getDataFromAPI']),
randomMethod() {
// Now I can Use my state
const data = this.$store.state.data;
}
}
};
Component B
export default {
name: 'Batch',
methods: {
randomMethodB() {
// If component A was loaded first than component B and A is B's parent, then the state will be accessible in the same manner and it should be populated
const data = this.$store.state.data;
}
}
};
Actions
const getDataFromAPI = ({ commit }) => new Promise((resolve, reject) => {
// Call server
const data = await callServer();
commit('updateMyStateWithData');
resolve(data);
});
export default {
getDataFromAPI
}
Mutations
const mutations = {
updateMyStateWithData(state, newData) {
state.data = newData;
}
}
export default mutations;
Another thing that I do is to define getters, that way is a good approach to load data once, and inside the getter you update the data to return only the things that your UI needs.
I hope that it helps!