Prefetching data with Nuxt to improve loading time - vue.js

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 :)

Related

How to display object in a variable in Vue 3 script setup

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: ''
}
])

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.

Using Suspense component in vue 3 along with Options API

The docs https://vuejs.org/guide/built-ins/suspense.html#async-components says in order to use Suspense Components, we need to make the components "Asynchronous".
<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>
<template>
{{ posts }}
</template>
But I am not sure how to make components Async using Options API.
With the Options API, the component is made an async component by using an async setup():
export default {
👇
async setup() {
const res = await fetch(...)
const posts = await res.json()
return {
posts
}
}
}
That's actually documented just above the docs section the question links to.
demo
As the documentation states, only async setup function and asynchronous script setup are supported. Options API is legacy and isn't supposed to receive new features.
As it can be seen in source code, only setup is supported.
It may be possible to access internal component instance and follow the way setup works, e.g.:
beforeCreate() {
this.$.asyncDep = promise;
},
But this is undocumented and hacky way that can become broken without notice.

v-on:click never firing on button in Nuxt component, because of middleware

I've seen this elsewhere in other questions, but haven't seen a solution. I'm trying to make a simple GDPR cookie notification that closes on click, using a button. I'm in Universal mode, meaning mounted() isn't available, so I'm trying to set cookies through Vuex. However, the click event I have bound to the button in my component isn't firing.
Edit: After building a codesandbox version of my app, which worked as it should, I went through and hacked up my Nuxt app until I found what was causing the problem. As it turns out, it was my middleware, and more specifically, the fact that I was using the fs-extra library to read JSON files. Still not clear on why this is happening, so any suggestions are welcome. The code below includes my middleware.
components/CookieNotice.vue
<template>
<div v-if="cookie != true" class="cookie_notice_wrap">
<div class="cookie_notice">
<p class="notice_message">This site uses cookies for analytics purposes.</p>
<button #click.prevent="dismissNotification" class="notice_dismiss">Close</button>
</div></div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "CookieNotice",
methods: {
dismissNotification: function(e) {
console.log("we clicked?");
document.querySelector(".cookie_notice_wrap").classList.add("hidden_click");
this.store.dispatch("cookieStateChange", true);
}
},
computed: {
...mapGetters(["cookie"]),
}
}
</script>
Actions from store/index.js
export const actions = {
async getPosts({ state, commit, dispatch }) {
if (state.posts.length) {
return
}
try {
await axios.get("/api/json-access");
}
catch (err) {
console.log(err);
}
},
nuxtServerInit({ state, commit, dispatch }, { req }) {
dispatch("getPosts");
const seen = this.$cookies.get("cookie_notice_seen");
if (!seen) {
dispatch("cookieStateChange", false);
}
},
cookieStateChange({state, commit, dispatch}, bool) {
// this does set the cookie correctly, unsure if it will work the same when bound to the button click
commit("updateCookie", bool);
this.$cookies.set("cookie_notice_seen", bool, {
path: "/",
maxAge: 60 * 60 * 24 * 7
});
}
}
~/middleware/api/json-access.js:
const fse = require('fs-extra');
import axios from 'axios';
const storeLocation = "middleware/full_site.json";
export default async function({ store, route, redirect }) {
const exists = await fse.pathExists(storeLocation);
let posts;
if (!exists) {
await fse.ensureFile(storeLocation);
posts = await postsFromWP();
fse.writeJSON(storeLocation, posts);
}
else {
try {
posts = await fse.readJSON(storeLocation);
}
catch (err) {
console.log(err);
}
}
store.commit("updatePosts", posts);
}
async function postsFromWP() {
const url = ".../example/json/file.json";
const config = { headers: { "Accept": "application/json" }};
let posts = await axios.get(url, config);
posts = posts.data
.filter(el => el.status === "publish")
.map(({ id, slug, title, excerpt, date, tags, content }) => ({
id, slug, title, excerpt, date, tags, content
}));
return posts;
}
I had the middleware configured in nuxt.config.js via routes -> middleware before, but currently have it set to go through serverMiddleware instead, for testing. I also added the action that triggers getting the posts, in case that's also part of it. This has definitely hit on a limit of my Nuxt/Vue understanding - I have no idea why this could be happening, so any wisdom is much appreciated.
If you wind up dealing with something similar, fs or fs-extra are your culprits. You can't use file operations on the client side, which is when these middleware actions are happening (I think). The cookie notice only fully worked when I removed the fs-extra import at the very top of the middleware file.

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