Maintaining Scroll Position and Pagination State in Vue.js for Seamless Navigation

Building seamless user experiences often involves ensuring that users can navigate within an application without losing their progress or state. In Vue.js applications, this is particularly relevant when working with paginated data. For example, users might navigate away to view documentation details and then want to return to the same page, at the same scroll position, and with the same pagination context.
In this article, we will explore how to preserve scroll position and pagination state in Vue.js, ensuring that the user returns to the same view and position upon navigation. This includes handling dynamic data, scroll tracking, and state management using techniques such as localStorage, Vue Router, or Vuex.
The Challenge
When building a paginated view, the main issues are:
Scroll Position: Preserving the user's vertical scroll position within the page.
Pagination State: Retaining the current page and number of items per page when navigating away.
View Restoration: Restoring both the scroll position and pagination state when returning to the view.
URL State Encoding: Optionally encoding pagination and state into the URL to make the view shareable and bookmarkable.
Solution Overview
To achieve this, we will:
Track and save the scroll position when the user navigates away from the page.
Preserve the pagination state using Vue Router query parameters, Vuex, or localStorage.
Restore both scroll and pagination states when the user returns to the page.
Implement an optional URL-based state system to enhance usability and allow for state sharing.
Implementation Details
1. Setting Up Pagination
First, we assume a basic Vue setup with paginated data. This component will manage the current page and items per page while fetching the appropriate data dynamically.
<template>
<div>
<div v-for="item in currentPageData" :key="item.id" class="item">
{{ item.title }}
</div>
<pagination
:currentPage="currentPage"
@pageChanged="handlePageChange"
/>
</div>
</template>
<script>
export default {
data() {
return {
currentPage: 1,
itemsPerPage: 10,
allData: [], // Full dataset
currentPageData: [] // Data for the current page
};
},
methods: {
fetchData() {
// Paginate data dynamically
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
this.currentPageData = this.allData.slice(start, end);
},
handlePageChange(page) {
this.currentPage = page;
this.fetchData();
}
},
mounted() {
this.fetchData();
}
};
</script>
In this setup:
currentPage
tracks the current page number.itemsPerPage
specifies how many items are shown per page.fetchData()
dynamically selects the data for the current page.
2. Preserving Scroll Position
The scroll position must be saved before navigation and restored when the user returns to the view. Use the window.scrollY
property to track the vertical scroll position.
Adding Scroll Position Handling
data() {
return {
scrollPosition: 0
};
},
methods: {
saveScrollPosition() {
this.scrollPosition = window.scrollY;
},
restoreScrollPosition() {
window.scrollTo(0, this.scrollPosition);
}
},
beforeRouteLeave(to, from, next) {
this.saveScrollPosition(); // Save the scroll position before leaving
next();
},
mounted() {
this.restoreScrollPosition(); // Restore the scroll position on mount
}
Here’s how it works:
saveScrollPosition
: Captures the current scroll position before navigating away.restoreScrollPosition
: Scrolls the window to the saved position when the component is mounted.
3. Saving and Restoring Pagination State
In addition to scroll position, the pagination state (current page, items per page) must also persist across navigation. This can be achieved using localStorage, Vuex, or Vue Router query parameters.
Option A: Using LocalStorage
methods: {
savePaginationState() {
localStorage.setItem("currentPage", this.currentPage);
localStorage.setItem("itemsPerPage", this.itemsPerPage);
},
restorePaginationState() {
const savedPage = localStorage.getItem("currentPage");
const savedItemsPerPage = localStorage.getItem("itemsPerPage");
if (savedPage) this.currentPage = parseInt(savedPage);
if (savedItemsPerPage) this.itemsPerPage = parseInt(savedItemsPerPage);
}
},
beforeRouteLeave(to, from, next) {
this.savePaginationState();
next();
},
mounted() {
this.restorePaginationState();
this.fetchData();
}
Option B: Using Vuex
If you're already using Vuex for state management, you can globally store the pagination state.
// Vuex Store
export default new Vuex.Store({
state: {
currentPage: 1,
itemsPerPage: 10
},
mutations: {
setPaginationState(state, { page, itemsPerPage }) {
state.currentPage = page;
state.itemsPerPage = itemsPerPage;
}
}
});
In your component:
methods: {
savePaginationState() {
this.$store.commit("setPaginationState", {
page: this.currentPage,
itemsPerPage: this.itemsPerPage
});
},
restorePaginationState() {
this.currentPage = this.$store.state.currentPage;
this.itemsPerPage = this.$store.state.itemsPerPage;
}
},
beforeRouteLeave(to, from, next) {
this.savePaginationState();
next();
},
mounted() {
this.restorePaginationState();
this.fetchData();
}
Option C: Using Vue Router Query Parameters
Encode the pagination state into the URL query:
methods: {
updateURL() {
this.$router.push({
query: {
page: this.currentPage,
itemsPerPage: this.itemsPerPage
}
});
},
fetchStateFromURL() {
const query = this.$route.query;
this.currentPage = query.page ? parseInt(query.page) : 1;
this.itemsPerPage = query.itemsPerPage ? parseInt(query.itemsPerPage) : 10;
}
},
watch: {
currentPage: "updateURL",
itemsPerPage: "updateURL"
},
mounted() {
this.fetchStateFromURL();
this.fetchData();
}
This approach enables users to share or bookmark the exact state of the application.
4. Combining Scroll and Pagination State
Here’s the complete example combining both scroll position and pagination state management:
<template>
<div>
<div v-for="item in currentPageData" :key="item.id" class="item">
{{ item.title }}
</div>
<pagination
:currentPage="currentPage"
@pageChanged="handlePageChange"
/>
</div>
</template>
<script>
export default {
data() {
return {
scrollPosition: 0,
currentPage: 1,
itemsPerPage: 10,
allData: [],
currentPageData: []
};
},
methods: {
fetchData() {
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
this.currentPageData = this.allData.slice(start, end);
},
saveState() {
this.scrollPosition = window.scrollY;
localStorage.setItem("currentPage", this.currentPage);
localStorage.setItem("itemsPerPage", this.itemsPerPage);
},
restoreState() {
this.currentPage = parseInt(localStorage.getItem("currentPage")) || 1;
this.itemsPerPage = parseInt(localStorage.getItem("itemsPerPage")) || 10;
this.scrollPosition = parseInt(localStorage.getItem("scrollPosition")) || 0;
this.fetchData();
window.scrollTo(0, this.scrollPosition);
}
},
beforeRouteLeave(to, from, next) {
this.saveState();
next();
},
mounted() {
this.restoreState();
}
};
</script>
Conclusion
By combining techniques for scroll tracking, pagination state preservation, and Vue Router query parameters, you can create a polished, user-friendly experience that seamlessly restores state across navigation. Whether using localStorage, Vuex, or query parameters, the approach should fit the complexity and requirements of your Vue.js application.
Subscribe to my newsletter
Read articles from Ahmed Raza directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ahmed Raza
Ahmed Raza
Ahmed Raza is a versatile full-stack developer with extensive experience in building APIs through both REST and GraphQL. Skilled in Golang, he uses gqlgen to create optimized GraphQL APIs, alongside Redis for effective caching and data management. Ahmed is proficient in a wide range of technologies, including YAML, SQL, and MongoDB for data handling, as well as JavaScript, HTML, and CSS for front-end development. His technical toolkit also includes Node.js, React, Java, C, and C++, enabling him to develop comprehensive, scalable applications. Ahmed's well-rounded expertise allows him to craft high-performance solutions that address diverse and complex application needs.