46. History API: Navigation Without Page Reload
Introduction
The History API allows manipulating the browser history and URL without reloading the page. This API is an important foundation for building Single Page Applications (SPAs) where navigation between pages happens seamlessly without a full refresh.
For IT students, understanding the History API is very important because almost all modern frameworks (React, Vue, Angular) use this concept for routing. By mastering the basics of the History API, your understanding of routing becomes more in-depth.
Why Is the History API Important?
Imagine an academic portal that has to reload the entire page every time a student switches from the Dashboard to the Grade List to the Course Schedule. The user experience would feel slow and unresponsive. The History API solves this problem by allowing partial updates of the URL and page content.
Main History API Methods
history.pushState()
This method adds a new entry to the browser history and changes the URL without reloading.
// Push a new state to the history
history.pushState(
{ page: 'dashboard', userId: 123 }, // state object
'Dashboard', // title (ignored by modern browsers)
'/portal/dashboard' // new URL
);
// Push a state with dynamic parameters
function navigateToCourse(courseId) {
history.pushState(
{ courseId: courseId },
`Kursus ${courseId}`,
`/portal/courses/${courseId}`
);
// Render the new content based on the URL
renderCoursePage(courseId);
}
// Check the current state
console.log(history.state); // { courseId: 123 }
console.log(window.location.pathname); // '/portal/courses/123'
history.replaceState()
This method replaces the current history entry without adding a new entry.
// Replace the current state
history.replaceState(
{ loggedIn: true },
'Portal Academic',
'/portal/welcome'
);
// Useful when updating state without navigation
function updateUserProfile(profileData) {
// Update state without pushing new history
history.replaceState(
{ ...history.state, profile: profileData },
'',
null // URL stays the same
);
}
// Example: redirect after login
function handleLoginSuccess(response) {
localStorage.setItem('token', response.token);
history.replaceState(
{ authenticated: true },
'Dashboard',
'/portal/dashboard'
);
renderDashboard();
}
history.back(), history.forward(), history.go()
// Go back to the previous page
history.back();
// Move forward to the next page
history.forward();
// Navigate to a specific entry relative to the current position
history.go(-2); // Go back 2 pages
history.go(1); // Go forward 1 page
history.go(0); // Reload the current page
The popstate Event
The popstate event is triggered when the user presses the browser's back/forward button.
// Listen for navigation changes from the back/forward buttons
window.addEventListener('popstate', (event) => {
const state = event.state;
if (state) {
// Navigate based on the state
switch (state.page) {
case 'dashboard':
renderDashboard();
break;
case 'courses':
renderCourseList();
break;
case 'course_detail':
renderCourseDetail(state.courseId);
break;
default:
render404();
}
} else {
// A null state means the initial page
renderHomePage();
}
});
// Example: initialize the page based on the current URL
function initRouter() {
const path = window.location.pathname;
window.addEventListener('popstate', (event) => {
routeTo(event.state);
});
// Render the first page
routeTo(history.state || { page: getInitialPage(path) });
}
function routeTo(state) {
if (!state) {
renderHomePage();
return;
}
document.getElementById('app').innerHTML = '';
switch (state.page) {
case 'dashboard':
renderDashboard();
break;
case 'courses':
renderCourses(state.filter);
break;
case 'profile':
renderProfile(state.userId);
break;
default:
render404();
}
}
Real-World Example: SPA Navigation for an Academic Portal
class AcademicPortal {
constructor() {
this.routes = {};
this.currentPage = null;
// Listen for the back/forward buttons
window.addEventListener('popstate', (e) => {
this.handleRoute(e.state);
});
// Catch clicks on internal links
document.addEventListener('click', (e) => {
const link = e.target.closest('a[data-link]');
if (link) {
e.preventDefault();
this.navigate(link.getAttribute('href'));
}
});
}
addRoute(path, handler) {
this.routes[path] = handler;
return this;
}
navigate(url, state = {}) {
// Push a new state to the history
history.pushState(state, '', url);
// Render the new page
this.handleRoute(state);
}
handleRoute(state) {
const path = window.location.pathname;
const handler = this.routes[path] || this.routes['*'];
if (handler) {
// Transition effect
const app = document.getElementById('app');
app.classList.add('page-transition');
setTimeout(() => {
handler(state);
app.classList.remove('page-transition');
}, 150);
}
}
start() {
this.handleRoute(history.state);
}
}
// Initialize the portal
const portal = new AcademicPortal();
// Register the routes
portal
.addRoute('/portal', (state) => {
renderNav('dashboard');
renderDashboard();
})
.addRoute('/portal/student', (state) => {
renderNav('students');
renderStudentList();
})
.addRoute('/portal/student/:id', (state) => {
renderNav('students');
renderStudentDetail(state.id);
})
.addRoute('/portal/lecture', (state) => {
renderNav('courses');
renderCourseList();
})
.addRoute('/portal/grade', (state) => {
renderNav('grades');
renderGradeSheet();
})
.addRoute('/portal/*', () => {
render404Page();
});
portal.start();
Example: Filtering and Sorting Without Reload
class CourseBrowser {
constructor() {
this.courses = [];
this.currentFilter = 'all';
this.currentSort = 'name';
window.addEventListener('popstate', (e) => {
if (e.state) {
this.currentFilter = e.state.filter || 'all';
this.currentSort = e.state.sort || 'name';
this.render();
}
});
}
filterCourses(category) {
this.currentFilter = category;
const state = {
filter: category,
sort: this.currentSort
};
const url = `/portal/courses?category=${category}&sort=${this.currentSort}`;
history.pushState(state, '', url);
this.render();
}
sortCourses(sortBy) {
this.currentSort = sortBy;
const state = {
filter: this.currentFilter,
sort: sortBy
};
const url = `/portal/courses?category=${this.currentFilter}&sort=${sortBy}`;
history.pushState(state, '', url);
this.render();
}
render() {
let filtered = [...this.courses];
// Apply filter
if (this.currentFilter !== 'all') {
filtered = filtered.filter(c => c.category === this.currentFilter);
}
// Apply sort
filtered.sort((a, b) => {
switch (this.currentSort) {
case 'name': return a.name.localeCompare(b.name);
case 'credits': return b.credits - a.credits;
case 'rating': return b.rating - a.rating;
default: return 0;
}
});
this.renderCourseGrid(filtered);
this.updateFilterButtons();
this.updateSortButtons();
}
renderCourseGrid(courses) {
const grid = document.getElementById('course-grid');
grid.innerHTML = courses.map(course => `
<div class="course-card" data-id="${course.id}">
<img src="${course.thumbnail}" alt="${course.name}">
<h3>${course.name}</h3>
<p>${course.description}</p>
<div class="course-meta">
<span>${course.credits} credits</span>
<span>⭐ ${course.rating}</span>
</div>
</div>
`).join('');
}
updateFilterButtons() {
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.category === this.currentFilter);
});
}
updateSortButtons() {
document.querySelectorAll('.sort-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.sort === this.currentSort);
});
}
}
// Usage
const browser = new CourseBrowser();
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
browser.filterCourses(btn.dataset.category);
});
});
document.querySelectorAll('.sort-btn').forEach(btn => {
btn.addEventListener('click', () => {
browser.sortCourses(btn.dataset.sort);
});
});
Example: Tab Navigation for a Course Details Page
class CourseDetail {
constructor(courseId) {
this.courseId = courseId;
this.currentTab = 'overview';
window.addEventListener('popstate', (e) => {
if (e.state && e.state.tab) {
this.switchTab(e.state.tab, false);
}
});
}
switchTab(tabName, pushState = true) {
this.currentTab = tabName;
if (pushState) {
history.pushState(
{ courseId: this.courseId, tab: tabName },
'',
`/portal/courses/${this.courseId}?tab=${tabName}`
);
}
// Update the tab UI
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
// Render the tab content
const content = document.getElementById('tab-content');
content.innerHTML = '';
switch (tabName) {
case 'overview':
this.renderOverview(content);
break;
case 'materials':
this.renderMaterials(content);
break;
case 'assignments':
this.renderAssignments(content);
break;
case 'grades':
this.renderGrades(content);
break;
case 'participants':
this.renderParticipants(content);
break;
}
}
renderOverview(container) {
container.innerHTML = `
<div class="overview">
<h3>Summary Mata Lecture</h3>
<p>Full description of this course...</p>
<div class="info-grid">
<div class="info-item">
<label>Lecturer Pengampu</label>
<span>Dr. John Smith, M.Kom</span>
</div>
<div class="info-item">
<label>credits</label>
<span>3 credits</span>
</div>
<div class="info-item">
<label>Jadwal</label>
<span>Monday & Wednesday, 08:00-09:30</span>
</div>
</div>
</div>
`;
}
renderMaterials(container) {
const materials = this.getMaterials();
container.innerHTML = `
<div class="materials">
<h3>Course Materials</h3>
<div class="material-list">
${materials.map(m => `
<div class="material-item">
<span class="material-icon">📄</span>
<div class="material-info">
<h4>${m.title}</h4>
<p>${m.date}</p>
</div>
<a href="${m.url}" class="download-btn">Download</a>
</div>
`).join('')}
</div>
</div>
`;
}
renderAssignments(container) {
const assignments = this.getAssignments();
container.innerHTML = `
<div class="assignments">
<h3>Assignment</h3>
<table class="assignment-table">
<thead>
<tr>
<th>Name Assignment</th>
<th>Deadline</th>
<th>Status</th>
<th>Grade</th>
</tr>
</thead>
<tbody>
${assignments.map(a => `
<tr>
<td>${a.name}</td>
<td>${formatDate(a.deadline)}</td>
<td><span class="status-${a.status}">${a.statusText}</span></td>
<td>${a.grade || '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
renderGrades(container) {
container.innerHTML = `
<div class="grades">
<h3>Grade List</h3>
<div class="grade-summary">
<div class="grade-card">
<h4>Midterm</h4>
<span class="grade-value">85</span>
</div>
<div class="grade-card">
<h4>Final</h4>
<span class="grade-value">90</span>
</div>
<div class="grade-card">
<h4>Assignment</h4>
<span class="grade-value">88</span>
</div>
<div class="grade-card final">
<h4>Final Grade</h4>
<span class="grade-value">87</span>
</div>
</div>
</div>
`;
}
renderParticipants(container) {
const students = this.getParticipants();
container.innerHTML = `
<div class="participants">
<h3>Peserta (${students.length} student)</h3>
<div class="student-grid">
${students.map(s => `
<div class="student-card">
<img src="${s.avatar}" alt="${s.name}">
<span>${s.name}</span>
<small>${s.studentId}</small>
</div>
`).join('')}
</div>
</div>
`;
}
}
Example: Multi-Step Form Wizard
class RegistrationWizard {
constructor() {
this.currentStep = 1;
this.totalSteps = 4;
this.formData = {};
window.addEventListener('popstate', (e) => {
if (e.state && e.state.step) {
this.goToStep(e.state.step, false);
}
});
}
goToStep(step, pushState = true) {
if (step < 1 || step > this.totalSteps) return;
this.currentStep = step;
if (pushState) {
history.pushState(
{ step: step, formData: this.formData },
'',
`/application/step/${step}`
);
}
this.renderStep();
this.updateProgressBar();
this.updateButtons();
}
nextStep() {
if (this.validateCurrentStep()) {
this.saveCurrentStepData();
this.goToStep(this.currentStep + 1);
}
}
prevStep() {
this.saveCurrentStepData();
this.goToStep(this.currentStep - 1);
}
renderStep() {
const container = document.getElementById('wizard-content');
switch (this.currentStep) {
case 1:
container.innerHTML = this.renderPersonalInfo();
break;
case 2:
container.innerHTML = this.renderAcademicInfo();
break;
case 3:
container.innerHTML = this.renderDocuments();
break;
case 4:
container.innerHTML = this.renderConfirmation();
break;
}
}
renderPersonalInfo() {
const data = this.formData.personal || {};
return `
<div class="wizard-step">
<h3>Informasi Pribadi</h3>
<div class="form-group">
<label for="name">Full name</label>
<input type="text" id="name" value="${data.name || ''}" required>
</div>
<div class="form-group">
<label for="studentId">StudentID</label>
<input type="text" id="studentId" value="${data.studentId || ''}" required>
</div>
<div class="form-group">
<label for="email">Email Campus</label>
<input type="email" id="email" value="${data.email || ''}" required>
</div>
<div class="form-group">
<label for="phone">No. HP</label>
<input type="tel" id="phone" value="${data.phone || ''}" required>
</div>
<div class="form-group">
<label for="address">Address</label>
<textarea id="address" required>${data.address || ''}</textarea>
</div>
</div>
`;
}
renderAcademicInfo() {
const data = this.formData.academic || {};
return `
<div class="wizard-step">
<h3>Informasi Academic</h3>
<div class="form-group">
<label for="faculty">Department</label>
<select id="faculty" required>
<option value="">Select Department</option>
<option value="engineering" ${data.faculty === 'engineering' ? 'selected' : ''}>Faculty of Engineering</option>
<option value="science" ${data.faculty === 'science' ? 'selected' : ''}>Faculty of Science</option>
<option value="FEB" ${data.faculty === 'FEB' ? 'selected' : ''}>Economics Department</option>
</select>
</div>
<div class="form-group">
<label for="majorCode">Major</label>
<select id="majorCode" required>
<option value="">Select Major</option>
</select>
</div>
<div class="form-group">
<label for="year">Enrollment Year</label>
<input type="number" id="year" value="${data.year || ''}" min="2018" max="2024" required>
</div>
<div class="form-group">
<label for="ips">Final GPA</label>
<input type="number" id="ips" value="${data.ips || ''}" min="0" max="4" step="0.01" required>
</div>
</div>
`;
}
renderDocuments() {
const data = this.formData.documents || {};
return `
<div class="wizard-step">
<h3>Upload Document</h3>
<div class="form-group">
<label for="transcript">Transcript Grade</label>
<input type="file" id="transcript" accept=".pdf" required>
<small>Format: PDF, Maks: 2MB</small>
</div>
<div class="form-group">
<label for="ktp">KTP</label>
<input type="file" id="ktp" accept=".jpg,.png" required>
<small>Format: JPG/PNG, Maks: 1MB</small>
</div>
<div class="form-group">
<label for="passportPhoto">3x4 Passport Photo</label>
<input type="file" id="passportPhoto" accept=".jpg,.png" required>
<small>Format: JPG/PNG, Maks: 500KB</small>
</div>
<div class="form-group">
<label for="recommendation_letter">Recommendation Letter</label>
<input type="file" id="recommendation_letter" accept=".pdf" required>
<small>Format: PDF, Maks: 1MB</small>
</div>
</div>
`;
}
renderConfirmation() {
return `
<div class="wizard-step">
<h3>Confirm Data</h3>
<div class="confirmation-section">
<h4>Informasi Pribadi</h4>
<p><strong>Name:</strong> ${this.formData.personal?.name || '-'}</p>
<p><strong>StudentID:</strong> ${this.formData.personal?.studentId || '-'}</p>
<p><strong>Email:</strong> ${this.formData.personal?.email || '-'}</p>
</div>
<div class="confirmation-section">
<h4>Informasi Academic</h4>
<p><strong>Department:</strong> ${this.formData.academic?.faculty || '-'}</p>
<p><strong>Major:</strong> ${this.formData.academic?.majorCode || '-'}</p>
<p><strong>GPA:</strong> ${this.formData.academic?.ips || '-'}</p>
</div>
<div class="confirmation-section">
<h4>Document</h4>
<p>All documents have been uploaded ✓</p>
</div>
</div>
`;
}
updateProgressBar() {
const progress = (this.currentStep / this.totalSteps) * 100;
document.getElementById('progress-bar').style.width = `${progress}%`;
document.querySelectorAll('.step-indicator').forEach((el, i) => {
el.classList.toggle('active', i + 1 === this.currentStep);
el.classList.toggle('completed', i + 1 < this.currentStep);
});
}
updateButtons() {
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const submitBtn = document.getElementById('submit-btn');
prevBtn.style.display = this.currentStep > 1 ? 'block' : 'none';
nextBtn.style.display = this.currentStep < this.totalSteps ? 'block' : 'none';
submitBtn.style.display = this.currentStep === this.totalSteps ? 'block' : 'none';
}
saveCurrentStepData() {
switch (this.currentStep) {
case 1:
this.formData.personal = {
name: document.getElementById('name').value,
studentId: document.getElementById('studentId').value,
email: document.getElementById('email').value,
phone: document.getElementById('phone').value,
address: document.getElementById('address').value
};
break;
case 2:
this.formData.academic = {
faculty: document.getElementById('faculty').value,
majorCode: document.getElementById('majorCode').value,
year: document.getElementById('year').value,
ips: document.getElementById('ips').value
};
break;
}
}
validateCurrentStep() {
const stepEl = document.querySelector('.wizard-step');
const inputs = stepEl.querySelectorAll('[required]');
let valid = true;
inputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('error');
valid = false;
} else {
input.classList.remove('error');
}
});
return valid;
}
}
// Usage
const wizard = new RegistrationWizard();
document.getElementById('next-btn').addEventListener('click', () => wizard.nextStep());
document.getElementById('prev-btn').addEventListener('click', () => wizard.prevStep());
document.getElementById('submit-btn').addEventListener('click', () => wizard.submit());
wizard.goToStep(1);
Example: Search with URL Updates
class StudentSearch {
constructor() {
this.searchInput = document.getElementById('search');
this.debounceTimer = null;
// Listen for popstate
window.addEventListener('popstate', (e) => {
if (e.state && e.state.search) {
this.searchInput.value = e.state.search;
this.performSearch(e.state.search, false);
}
});
// Handle input
this.searchInput.addEventListener('input', (e) => {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.performSearch(e.target.value);
}, 300);
});
}
performSearch(query, pushState = true) {
if (pushState && query) {
history.pushState(
{ search: query },
'',
`/portal/student/search?q=${encodeURIComponent(query)}`
);
} else if (pushState && !query) {
history.pushState(
{ search: '' },
'',
'/portal/student'
);
}
const results = this.filterStudents(query);
this.renderResults(results);
}
filterStudents(query) {
const students = this.getAllStudents();
if (!query) return students;
const lowerQuery = query.toLowerCase();
return students.filter(s =>
s.name.toLowerCase().includes(lowerQuery) ||
s.studentId.includes(query) ||
s.majorCode.toLowerCase().includes(lowerQuery)
);
}
renderResults(results) {
const container = document.getElementById('search-results');
if (results.length === 0) {
container.innerHTML = '<p class="no-results">No students found</p>';
return;
}
container.innerHTML = `
<div class="results-info">
<p>Found ${results.length} students</p>
</div>
<div class="student-table">
<table>
<thead>
<tr>
<th>StudentID</th>
<th>Name</th>
<th>Major</th>
<th>Year</th>
<th>GPA</th>
</tr>
</thead>
<tbody>
${results.map(s => `
<tr>
<td>${s.studentId}</td>
<td>${s.name}</td>
<td>${s.majorCode}</td>
<td>${s.year}</td>
<td>${s.gpa}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
}
Code Explanation
Main Methods
history.pushState(state, title, url)- Adds a new history entry and changes the URLhistory.replaceState(state, title, url)- Replaces the current history entryhistory.back()- Navigates to the previous pagehistory.forward()- Navigates to the next pagehistory.go(n)- Navigates relatively by n steps
Event
popstate- Triggered when the user presses the browser's back/forward button
State Object
history.state- Access the stored state object- Can contain any JSON data (maximum ~640KB depending on browser)
- Cannot contain functions or objects that cannot be serialized
URL Updates
- The URL must be same-origin (no cross-origin)
- The path must be valid and parseable by the server
- The query string and hash can also be updated
Conclusion
The History API is the foundation for building a good Single Page Application. By mastering pushState, replaceState, and the popstate event, you can create seamless navigation without page reloads. For an academic portal, this means a faster and more responsive user experience when students browse through various pages such as the grade list, course schedule, and other academic information. Always remember to handle the browser's back/forward cases and store enough state to re-render a page from the URL.