How to Suppress Fetch Occurring in Parent View (Vue.js) - vue.js

I have a View.js application that includes two views - Articles.vue and Article.vue - such that Article.vue is nested within Articles. Here is how these views behave:
When the Articles created() lifecycle event fires, this view fetches a list of accounts via HTTP and renders links to Article.vue within a v-for loop.
A user can then click on an Article in the Articles view. This links to the child Articles.vue, which then shows details of the Article.
The issue that I am having is that when step 2 occurs, the Articles view recreates (i.e. the created() event re-fires, and the Articles list is unnecessarily re-retrieved.) This causes an unwanted delay and screen flash that makes the app feel less responsive.
What I would like is for the Articles view to render only once, and not each time an Article is clicked on. How might I accomplish this?
I do get the desired behavior if I include the Articles component directly in the home page, instead of linking to it as a view from the home page. However, since the Articles view is only intended to appear when the user selects this functionality, this direct inclusion is not a viable approach.
Also, I have come upon keep-alive, which I see will prevent the parent route from re-rendering. However, keep-alive is be too broad, as I do want the parent view to reload if the parent vue link is clicked again (i.e. to retrieve new Articles); I just don't want the view to reload if the child vue link is clicked.
Possibly, the answer lies in Navigation Guards:
https://router.vuejs.org/guide/advanced/navigation-guards.html
However, I am new to Vue and am not able to parse documentation on this feature to arrive at my solution.
Following is the relevant code:
Articles
<template>
<div class="hello">
<br />
<div v-for="Article in Articles" :key="Article.ArticleID">
<router-link :to="{name: 'Article', params: {'ArticleID': Article.ArticleID}}">{{Article.ArticleName}}</router-link>
</div>
<br />
<router-view :key="$route.path"></router-view>
</div>
</template>
<script>
export default {
name: "Articles",
data() {
return {
Articles: []
}
},
mounted() {
this.getArticles();
},
methods: {
getArticles: function() {
console.log("Getting data");
const url = "http://localhost:8001/articles"
fetch(url)
.then( (response) => {
console.log("Converting data to json");
return response.json();
})
.then( (data) => {
if (typeof(data[0].ArticleID)=="undefined") {
console.log("No data found.");
} else
{
console.log("Data found");
this.Articles = data;
}
})
.catch( err => console.log("Error retrieving data: " + err));
}
}
}
</script>
Routes Section
{
path: "/articles",
name: "Articles",
component: () =>
import(/* webpackChunkName: "y" */ "../views/Articles.vue"),
children: [
{
path: "/article/:ArticleID",
name: "Article",
props: true,
component: () =>
import(/* webpackChunkName: "z" */ "../components/Article.vue")
}
]
}

Related

VUE3 pass data between files

In Home.vue I get data from db.json and store it into jobs: [] array.
export default {
name: 'Home',
data() {
return {
jobs: [],
}
},
components: {
},
mounted() {
fetch("http://localhost:3000/jobs")
.then(res => res.json())
.then(data => this.jobs = data)
.catch(err => console.log(err.message))
}
}
Also in Home.vue I show this data, but only in a short list with:
v-for="job in jobs.slice(0, 5)"
In AllJobs.vue I want to show the full data from db.json and in AddJob.vue I will make a form to be able to add data to db.json.
In App.vue I have the router-links:
<template>
<div class="container">
<div class="navigation">
<h1 class="title">{{ $route.name }}</h1>
<nav>
<router-link :to="{ name: 'Latest open positions' }">Home</router-link>
<router-link :to="{ name: 'All open positions' }">Jobs</router-link>
<router-link :to="{ name: 'Add a new job' }">Dashboard</router-link>
</nav>
</div>
<router-view/>
</div>
</template>
How I pass data from Home.vue into AllJobs.vue?
Should I get another fetch method into AllJobs.vue to get data?
Should I get data into App.vue and then pass it into files that I need?
What is the best approach?
When it comes to handling API requests and sharing data between components, what you need is some state management solution like pinia.
You can fetch and save your data in a store and then use it in any component:
jobs.js
export const useJobsStore = defineStore('jobs', {
state: () => ({ jobs: [] }),
actions: {
fetchJobs() {
fetch("http://localhost:3000/jobs")
.then(res => res.json())
.then(data => this.jobs = data)
.catch(err => console.log(err.message))
},
},
})
App.vue
import { mapActions } from 'pinia
import { useJobsStore } from './jobs.js'
export default {
methods: {
...mapActions(useJobsStore, ['fetchJobs'])
},
mounted() {
this.fetchJobs()
}
}
Home.vue and AllJobs.vue
import { mapState } from 'pinia'
import { useJobsStore } from './jobs.js'
export default {
computed: {
// this makes this.jobs available in script and template
...mapState(useJobsStore, ['jobs'])
}
}
One thing which is debatable is where to call fetchJobs action
In App.vue or main.js - this will fetch data as soon as you open the app, but can be unnecessary if the page you visit doesn't even use the data.
In each page that uses the data - solves the previous problem, but fetches the same data multiple times.
In each page that uses the data (with caching) - you can modify fetchJobs to make a request only if the data haven't been fetched already. This way the app will fetch the data as soon as you visit some page which uses it. And if you visit another page, it will use the cached value instead of making a request
There isn't a singe best approach, which one to pick depends on your needs

Vue router access meta data from router-link

I use vue3 and vue-router-4, and am trying to hide some router links for unauthorized users:
Route file:
{
path: "/users/create",
name: "UserCreate",
meta: {
title: "Create user",
requiresAuth: true
},
component: () =>
import(/* webpackChunkName: "create" */ "#/views/Users/Create.vue")
}
As you can see I have requiresAuth: true meta in this route, so I want to hide this for guests.
I want to use something like this in my views (v-if part, which is not working):
<router-link v-if="route.meta.requiresAuth === isLoggedIn()" to="/users/create" class="nav-link"
>Create user</router-link>
Please advise how I can achieve this, and if it's not possible with meta fields - what is the preferred way to hide links.
P.S. Of course all validation and checks of accesses will be performed at the back-end side, but I still don't want to show user links which they can't view.
Keep in mind that this only hides the links, users can still access these routes through the URL. you need to use the before navigation guard in addition to hiding links:
Navigation guard :
//router.js
router.beforeEach((to,from,next)=>{
if ( to.matched.some(record => record.meta.requiresAuth) {
if( isLoggedIn()) {
next()
} else {
// if user is not logged what should happen
}
} else {
next()
}
})
Hide links if they require Auth and the user is not authenticated:
<router-link v-if="isLoggedIn()" to="/users/create" class="nav-link">
Create user
</router-link>

Where should route meta data be loaded in a Vue app?

I'm in the process of setting up a VueJs SPA. I'm using vue-router and I'm trying to find the best solution to the following problem. I have a series of routes. Each of which needs to call an API to get the meta data for the given ID.
/industry/:id/overview
/industry/:id/top-stories
/industry/:id/top-tweets
/brand/:id/overview
/brand/:id/top-stories
/brand/:id/top-tweets
I've been looking at using created or beforeRouteEnter/beforeRouteUpdate and I'm a bit lost. Ideally, I would only fetch new data when a new /industry/:id is reached, not when navigating between pages within the same ID. Also, I'd like to avoid having to define the fetch to grab data in every page component. Also don't want to over complicate this, so my question is, Is there a standard method for tackling this issue?
Clarification:
When I say meta here, I mean data returned from an API about the given industry or brand which I pull using the ID in the route. The api call includes the name of the industry/brand which I want to have on page as soon as the page is presented to the user.
I have something similar. I tackle this using the following approach:
I use the same component for all /industry/:id Vue likes to reuse components wherever it can so if two routes (for example /industry/:id/overview and /industry/:id/top-stories) are using the same component it will stay the same.
What does change, however, is the route meta. So if you add a page key to the meta object in the route objects, and probably add a computed property called page that return this.$route.meta.page, you can use v-if attributes to conditionally render any component. So you might have something like <div v-if="page === 'overview'"></div><div v-else-if="page==='top-stories'"></div>
What this allows you to do is fetch all the data from the API during created or mounted lifecycle and store it as the state. Since the route change doesn't reload the component the state stays the same.
Here is a code example
// router.js
const Project = () =>
import(/* webpackChunkName: "projects" */ "./views/projects/_id");
export default new Router({
mode: "history",
routes: [
{
path: "/projects/:project_id/views",
name: "ViewProject",
component: Project,
meta: {
page: "views",
}
},
{
path: "/projects/:project_id/export",
name: "ExportProject",
component: Project,
meta: {
page: "exports"
}
},
{
path: "/projects/:project_id/recommendations",
name: "ProjectRecommendations",
component: Project,
meta: {
page: "recommendations"
}
},
]
});
And here is the template
<template>
<div v-if="project">
<h1>{{ project.name }}</h1>
<router-link :to="/project/someid/views">Views</router-link>
<router-link :to="/project/someid/exports">Exports</router-link>
<router-link :to="/project/someid/recommendations">Recommendations</router-link>
<ul v-if="page==='views">
<li v-for="(view, i) in project.views" :key="i">{{ views }}</div>
</ul>
<ul v-else-if="page==='exports">
<li v-for="(export, i) in project.exports" :key="i">{{ export }}</div>
</ul>
<ul v-else-if="page==='recommendations">
<li v-for="(recommendation, i) in project.recommendations" :key="i">{{ recommendation }}</div>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
project: null
}
},
computed: {
page() {
return this.$route.meta.page;
}
},
mounted() {
this.getProject()
},
methods: {
getProject() {
axios
.get(`/projects/someid`)
.then(res => this.project = res.data)
}
}
}
</script>

Experiencing navbar flicker with Vue.js

I'm experiencing a flicker in my navbar before a function is evaluated to either true or false.
The function that needs to evaluate is the following:
export default {
methods: {
isAuthenticated () {
return this.$store.state.user.authenticated
}
},
data: () => {
return {
unauthenticated: [
{
title: 'Link1',
url: '/link1'
},
{
title: 'Link2',
url: '/link2'
},
{
title: 'Link3',
url: '/link3'
}
],
authenticated: [
{
title: 'otherLink1',
url: '/otherlink1'
},
{
title: 'otherLink2',
url: '/otherlink2'
},
{
title: 'otherLink3',
url: '/otherlink3'
}
]
}
}
}
And the navbar has the following:
<template v-if="isAuthenticated()">
<b-nav is-nav-bar>
<b-nav-item v-for="nav in authenticated" :key="nav.title" :href="nav.url">{{nav.title}}</b-nav-item>
</b-nav>
</template>
<template v-else>
<b-nav is-nav-bar>
<b-nav-item v-for="nav in unauthenticated" :key="nav.title" :href="nav.url">{{nav.title}}</b-nav-item>
</b-nav>
</template>
However, when I click through the navigation, the unauthenticated links appear for a second and then the authenticated links appear as if the isAuthenticated() function hasn't evaluated yet. What can I do to remove this flicker?
My store file (user.js) file looks like this:
export const state = () => ({
headers: {},
profile: {}
})
export const mutations = {
updateHeaders (state, headers) {
state.headers.access_token = headers['access-token']
state.headers.token_type = headers['token-type']
state.headers.client = headers['client']
state.headers.expiry = headers['expiry']
state.headers.uid = headers['uid']
if (state.headers.expiry == null) {
state.authenticated = false
} else {
let timeToExpiry = new Date(state.headers.expiry * 1000)
let now = new Date()
state.authenticated = now < timeToExpiry
}
},
signout (state) {
state.headers = {}
state.profile = {}
}
}
The login/logout methods occur via API calls to a Rails app. The Devise gem handles the rest.
Thanks in advance!
EDIT:
I am using Nuxt.js for the layouts/pages/components so I believe that links submit with a this.$router.push(url) under the hood.
The b-nav tags are coming from Bootstrap Vue
When using bootstrap-vue there are two ways to add links to the navbar. One is to bind to :href attribute, which creates a regular html anchor. The other is to use :to attribute, which creates a link that interacts with vue-router.
<b-navbar-nav v-if="isAuthenticated()">
<b-nav-item v-for="nav in authenticated" :key="nav.title" :to="nav.url">{{nav.title}}</b-nav-item>
</b-navbar-nav>
<b-navbar-nav v-if="!isAuthenticated()">
<b-nav-item v-for="nav in unauthenticated" :key="nav.title" :to="nav.url">{{nav.title}}</b-nav-item>
</b-navbar-nav>
No reason to use <template> tags here to encapsulate the . Also note that 'is-nav-bar' is deprecated. See here where they note the deprecation.
What code executes when you click one of the links is not stated, I assume it's something like this.$router.push(url). If this is the case, you've probably have included your navbar in the <router-view>, so when you switch current route, components inside <router-view> rerender, so the navbar flashes. Move them out of the <router-view> should fix this.
edit: so the OP is not using vue-router yet, in this case, either manually change the root component's data to make parts other than the navs change, or add vue-router and use this.$router.push() to navigate so parts outside <router-view> won't change or flash.
Anyway, we need the vue component to stay to let vue to rerender only part of the view, while simply navigating by <a> or something will destruct everything and reconstruct them again, hence the flashing.

Navigating vuejs SPA via routes that share component does not refresh component data as expected

I have a couple routes in my vuejs SPA that I have set up using vue-router:
/create/feedback
/edit/feedback/66a0660662674061b84e8ea2fface0e4
The component for each route is the same form with a bit of smarts to change form values based on the absence or present of the ID in the route (feedbackID, in my example).
I notice that when I click from the edit route to the create route, the data in my form does not clear.
Below is the gist of my route file
import FeedbackFormView from './components/FeedbackForm.vue'
// Routes
const routes = [
{
path: '/create/feedback',
component: FeedbackFormView,
name: 'FeedbackCreate',
meta: {
description: 'Create Feedback',
}
},
{
path: '/edit/feedback/:feedbackId',
component: FeedbackFormView,
name: 'FeedbackEdit',
meta: {
description: 'Edit Feedback Form'
},
props: true
}
]
export default routes
Below is the gist of my component
<template lang="html">
<div>
<form>
<input v-model="model.someProperty">
</form>
</div>
</template>
<script>
export default {
data() => ({model: {someProperty:''}}),
props: ['feedbackId'],
created() => {
if (!this.$props['feedbackId']) {
return;
}
// otherwise do ajax call and populate model
// ... details omitted
}
}
</script>
However, if I modify my component as follows, everything works as expected
<template lang="html">
<div>
<form>
<input v-model="model.someProperty">
</form>
</div>
</template>
<script>
export default {
data() => ({model: {someProperty:''}}),
props: ['feedbackId'],
created() => {
if (!this.$props['feedbackId']) {
return;
}
// otherwise do ajax call and populate model
// ... details omitted
},
watch: {
'$route' (to, from) {
if (to.path === '/create/feedback') {
this.model = {}
}
}
}
}
</script>
Why is this? Why do I need watch?
I would have though that changing routes would be sufficient as the purpose of routing is to mimic the semantic behavior of page navigation
You have same component for different routes, when you go to edit route from the create route component is already created and mounted so the state of the component doesn't clear up.
Your component can listen to route changes using $router provided by vue-router every time the route changes the watcher is called.
For those who come this later, the following answer addresses the issue I was facing:
Vue-Router: view returning to login page after page refresh