I'm creating a Vue 2 website where the pages are linked in a linear workflow. Each page has its own route (e.g. /, /intro, /summary). I'm using Vuex to store the page number and total pages as well, so each page can show the text "Page X of Y" and use Next and Previous buttons to move forward and back in the workflow.
In my router, I use beforeEach to get the page # from the new route and set it on the store or redirect the user to the homepage. This commits a mutation because as far as I know, getting the route's path and looking up the page # of that route in an array are synchronous operations:
export default async ({ app }) => {
await app.router.beforeEach((to, from, next) => {
const pages = app.store.getters['pages/all']
const pageIndex = pages.indexOf(to.path)
if (pageIndex >= 0) {
app.store.commit('pages/set', pageIndex)
next()
} else {
next('/')
}
})
}
The Vuex store initializes the page index to 0 and uses an array to maintain the order of routes in the workflow:
export const state = () => ({
currentIndex: 0,
pages: [
'/',
'/intro',
'/summary'
]
})
export const mutations = {
set (state, newIndex) {
if (newIndex < 0) {
state.currentIndex = 0
} else if (newIndex >= state.pages.length) {
state.currentIndex = state.pages.length - 1
} else {
state.currentIndex = newIndex
}
}
}
However, when I navigate directly to /intro, the page shows "Page 1 of 3" for a moment before changing to "Page 2 of 3" as expected.
Since the mutation is a synchronous operation, I'm not sure why the content changes after the route is finalized and the page is drawn.
How can I adjust this solution so that the correct page number is shown when the page loads without showing an incorrect page # first?
Related
I'm quite new with Vue, and vue-router. Right now I'm working on a page where we organize tennis matches, I have this component called goBack, it basically checks your browser history entries and take you back to the previous one, similar to the back-arrow in your browser. So I have this behavior which I don't like, and I don't know how to change it => if I set match, pass you the URL, if you enter and click on my goBack Comp it will take you out of my page. I want to avoid that with this logic, if your last browser history entry is not inside my page I want to redirect you to my home.
Here's the goBack component code:
setup(props, { attrs }) {
const router = useRouter()
const { t } = useI18n()
const goBack = (): void => {
if (attrs.onClick) return
if (router.options.history.state.back === null) {
router.replace('/')
} else {
router.back()
}
}
return {
t,
goBack
}
}
})
</script>
Thank you!
With the first login in my app, users get a possibility to leave their address. When this address is stored, the user are pushed to their dashboard. Second login the user go straight to the dashboard.
I have 2 Vuex states that are updated with the response.data. 'Signed' leads to address page, 'Frequent' leads to 'dashboard'.
//PROMPT.VUE
mounted () {
this.getPrompt()
},
computed: {
promptStatus () {
return this.$store.getters.getPrompt
}
},
methods: {
async getPrompt() {
try{
await //GET axios etc
// push prompt status in Store
let value = response.data
this.$store.commit('setPrompt', value)
if (this.promptStatus === 'signed') {
this.$router.push({path: '/adres'})
}
if (this.promptStatus === 'frequent') {
this.$router.push({path: '/dashboard'})
}
When user leaves the address I reset the vuex.state from 'signed' to 'frequent'.
//ADRES.VUE
//store address
let value = 'frequent'
this.$store.commit('setPrompt', value)
this.$router.push({name: 'Prompt'})
The Vuex.store is refreshed. But the Prompt.vue wil not re-render with the new vuex.status. Many articles are written. Can 't find my solution. Maybe I organize my pages the wrong way.
In views, it is not recommended to mutate data (call commit) outside vuex. Actions are created for these purposes (called from the component using dispatch). In your case, you need to call action "getPrompt" from the store, but process routing in the authorization component. This is more about best practice
To solve your problem, you need to make a loader when switching to dashboard. Until the data is received, you do not transfer the user to the dashboard page
Example
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "DashboardLayout",
components: { ..., ... },
data: () => ({
isLoad: false
}),
async created() {
this.isLoad = false;
try {
await this.$store.dispatch('getData');
this.isLoad = true;
} catch (error) {
console.log(error)
}
}
});
</script>
Data is received and stored in the store in the "getData" action.
The referral to the dashboard page takes place after authorization. If authorization is invalid, the router.beforeEach handler (navigation guards) in your router/index.js should redirect back to the login page.
Learn more about layout in vuejs
Learn more about navigation guards
apologies for the simple question, I'm really new to Vue/Nuxt/Vuex.
I am currently having a vuex store, I wish to be able to populate the list with an API call at the beginning (so that I would be able to access it on all pages of my app directly from the store vs instantiating it within a component).
store.js
export const state = () => ({
list: [],
})
export const mutations = {
set(state, testArray) {
state.list = testArray
}
}
export const getters = {
getArray: state => {
return state.list
},
}
I essentially want to pre-populate state.list so that my components can call the data directly from vuex store. This would look something like that
db.collection("test").doc("test").get().then(doc=> {
let data = doc.data();
let array = data.array; // get array from API call
setListAsArray(); // put the array result into the list
});
I am looking for where to put this code (I assume inside store.js) and how to go about chaining this with the export. Thanks a lot in advance and sorry if it's a simple question.
(Edit) Context:
So why I am looking for this solution was because I used to commit the data (from the API call) to the store inside one of my Vue components - index.vue from my main page. This means that my data was initialized on this component, and if i go straight to another route, my data will not be available there.
This means: http://localhost:3000/ will have the data, if I routed to http://localhost:3000/test it will also have the data, BUT if i directly went straight to http://localhost:3000/test from a new window it will NOT have the data.
EDIT2:
Tried the suggestion with nuxtServerInit
Updated store.js
export const state = () => ({
list: [],
})
export const mutations = {
set(state, dealArray) {
state.list = dealArray
}
}
export const getters = {
allDeals: state => {
return state.list
},
}
export const actions = {
async nuxtServerInit({ commit }, { req }) {
// fetch your backend
const db = require("~/plugins/firebase.js").db;
let doc = await db.collection("test").doc("test").get();
let data = doc.data();
console.log("deals_array: ", data.deals_array); // nothing logged
commit('set', data.deals_array); // doesn't work
commit('deals/set', data.deals_array); // doesn't work
}
}
Tried actions with nuxtServerInit, but when logging store in another component it is an empty array. I tried to log the store in another component (while trying to access it), I got the following:
store.state: {
deals: {
list: []
}
}
I would suggest to either:
calling the fetch method in the default.vue layout or any page
use the nuxtServerInit action inside the store directly
fetch method
You can use the fetch method either in the default.vue layout where it is called every time for each page that is using the layout. Or define the fetch method on separate pages if you want to load specific data for individual pages.
<script>
export default {
data () {
return {}
},
async fetch ({store}) {
// fetch your backend
var list = await $axios.get("http://localhost:8000/list");
store.commit("set", list);
},
}
</script>
You can read more regarding the fetch method in the nuxtjs docs here
use the nuxtServerInit action inside the store directly
In your store.js add a new action:
import axios from 'axios';
actions: {
nuxtServerInit ({ commit }, { req }) {
// fetch your backend
var list = await axios.get("http://localhost:8000/list");
commit('set', list);
}
}
}
You can read more regarding the fetch method in the nuxtjs docs here
Hope this helps :)
I am building a kind of book app (Holy Quran )... user will go for a list of Surahs, each Surah contains around 5 - 50 pages. I managed navigating user from the list to first page of each Surahs.. and through getting an api request data for first page will be shown and this is the code in the showScreen
const [quran, setQuran] = useState([]);
const page = navigation.getParam('page');
const name = navigation.getParam('name');
let pageNumber = page;
useEffect(() => {
Quran();
}, []);
const Quran = async () => {
const response = await QuranApi.get(`/${pageNumber}/quran-uthmani`);
setQuran(response.data.data.ayahs);
}
so let's imagine that first page is page number 200, I am looking for some way so when user clicks go to page 201 or 199 (next or previous) and refetching the data so show for him requested page
I need some help here please and thanks in advance
Basically you need to add some sort of button or any element in your 'markup section' which will trigger next/previous action. For example:
// The following line makes react listen for changes in page state variable.
// If you use setPage() anywhere, react will auto update all the components
// where page variable is used. You don't need to manually do it
const [page, setPage] = useState(nav.getParam('page'))
// ....
// This function does exactly that
const handleClick = event => {
if(event.target.value === 'next-page') // If next button was pressed
setPage(page + 1) // We increment the page state variable by 1
else if(event.target.value === 'prev-page') // If prev button was pressed
setPage(page - 1) // We decrement the page state variable by 1
// By doing so, react will auto update every element which uses this variable
}
// .....
// We tell react, if [page] state variable changes by user clicking
// next or previous button, fetch data from the api using updated
// page number and using that, we update the [quran] variable using
// 'setQuran()' function
useEffect(async () => {
const response = await QuranApi.get(`/${page}/quran-uthmani`)
setQuran(response.data.data.ayahs)
}, [page] );
//......
// markup section
return(
//....
<Button onClick={handleClick} value="next-page">Next Page {page + 1}</Button>
<Button onClick={handleClick} value="prev-page">Prev Page {page - 1}</Button>
//....
)
Thank you dear #Sayed it finally works but I have to make some modifications with the same idea to be
const [page, setPage] = useState(navigation.getParam('page'))
const handleRightClick = () => {
setPage(parseInt(page) + 1)
};
const handleLeftClick = () => {
setPage(parseInt(page) - 1);
}
useEffect(() => {
return () => {
console.log("cleaned up");
};
}, []);
useEffect(() => {
Quran();
}, [page]);
<Button title = 'Next Page' onPress = {handleRightClick}/>
<Button title = 'Prev-page' onPress ={handleLeftClick} />
So it's working well in that case without an error
Try this small change...
const [page,setPage]=useState(nav.getParam('page'))
useEffect(() => {
Quran();
}, [page]);
by passing an empty array as a second parameter to the useEffect function, you are telling react to only execute the side effect once (at mount time).
However if that array contains a list of state variables to watch for. React will only re-run the side effect if one of the items in this array changes.
The above mentioned change in useEffect would make your useEffect sensitive to any change in the state variable (page) passed to the useEffect array. As you pass new pagenumbers in the url as a parameter, it would result in calling useEffect everytime as the page state variable is set.
Hope this helps!
the story:
I am on product page #/product/7 and on the same page I have 4 more products that are similar to the one that is being viewed. All these products have links to their pages:
router-link(:to="{ name: 'product', params: { id: product.id }}" v-text='product.title').
the problem:
When I click on any of the product links, the url changes but the content remains the same. So, if I am on #/product/7 and click on #/product/8 the url only will change. If I navigate from /product/:id page and click on a product it takes me to the right page with proper content.
As you can see on screenshot, current product id is 15, but the content is the one from the id 7, as shown in url at the bottom while I was hovering over the Sleek Silk Shirt product in cart.
Any ideas how to fix this?
You have to update the data of products variable when you change the route as vue optimises page reloads and does not reload in your case if you are on same route.
You can adapt the approach: Fetching Before Navigation described in vue-router docs:
With this approach we fetch the data before actually navigating to the new route. We can perform the data fetching in the beforeRouteEnter guard in the incoming component, and only call next when the fetch is complete:
export default {
data () {
return {
product: {},
error: null
}
},
beforeRouteEnter (to, from, next) {
getProduct(to.params.id, (err, product) => {
if (err) {
// display some global error message
next(false)
} else {
next(vm => {
vm.product = product
})
}
})
},
// when route changes and this component is already rendered,
// the logic will be slightly different.
watch: {
$route () {
this.product = {}
getProduct(this.$route.params.id, (err, product) => {
if (err) {
this.error = err.toString()
} else {
this.product = product
}
})
}
}
}
I couldnt really internalise the above answer with 'getProduct', so to be put simply.
I am using a Store and I needed to watch the $route and when it changes I called my store to dispatch the api call.
watch: {
$route () {
this.$store.dispatch('fetchStudyStandards',
this.$route.params.standardID);
}
}
So basically watch the route and if it changes, re do your api call.