Пошук уроків, статей та іншого контенту
Керуватимете історією браузера й навігацією без повного перезавантаження сторінки.
History API дає змогу керувати записами в історії браузера з JavaScript. Це потрібно, коли інтерфейс змінюється без повного перезавантаження сторінки, наприклад у SPA-застосунках.
За допомогою History API можна:
додавати нові записи в історію;
змінювати поточний запис;
переходити назад і вперед;
реагувати на навігацію кнопками браузера;
змінювати URL без завантаження нового документа.
Основні методи й події:
history.pushState() — додає новий запис;
history.replaceState() — замінює поточний запис;
history.back() — переходить назад;
history.forward() — переходить уперед;
history.go() — переходить на вказану кількість записів;
window.popstate — спрацьовує після переходу між записами історії.
У спрощеному вигляді історію вкладки можна уявляти як список URL:
/ ← поточний документ
/products
/products/42Коли користувач переходить на /products, а потім на /products/42, браузер додає два записи. Кнопка «Назад» повертає користувача до попереднього запису.
Звичайне посилання:
<a href="/products">Товари</a>зазвичай призводить до завантаження нового документа.
History API дає змогу додати запис до історії та самостійно оновити інтерфейс:
history.pushState(
{ page: "products" },
"",
"/products"
);Цей виклик:
змінює URL;
додає новий запис в історію;
не перезавантажує документ;
не викликає подію popstate одразу.
Останній пункт важливий: після pushState() відображення сторінки потрібно оновити самостійно.
pushState()Сигнатура методу:
history.pushState(state, unused, url);Перший аргумент — значення, яке буде пов’язане з новим записом історії:
history.pushState(
{ page: "profile", userId: 42 },
"",
"/users/42"
);Після повернення до цього запису через кнопку «Назад» або «Вперед» дані будуть доступні в event.state.
Об’єкт стану має бути серіалізованим алгоритмом structured clone. Зазвичай безпечно зберігати в ньому:
рядки;
числа;
булеві значення;
масиви;
звичайні об’єкти.
Не варто зберігати там DOM-вузли, функції або великі об’єкти стану застосунку. Часто в state достатньо покласти ідентифікатор маршруту:
history.pushState(
{ route: "/products/42" },
"",
"/products/42"
);Другий параметр історично називається unused. Браузери очікують, що він буде переданий, тому зазвичай використовують порожній рядок:
history.pushState({}, "", "/about");Третій аргумент — новий URL. Він може бути:
history.pushState({}, "", "/about");
history.pushState({}, "", "/products?page=2");
history.pushState({}, "", "?page=2");
history.pushState({}, "", "#details");Застосунок може змінювати лише URL того самого походження. Наприклад, сторінка з https://example.com не може через pushState() перейти на https://another.example.
replaceState()replaceState() має такий самий формат:
history.replaceState(state, unused, url);Але він не додає новий запис. Метод замінює поточний запис історії.
Це корисно, коли URL потрібно виправити або синхронізувати зі станом без створення зайвого кроку для кнопки «Назад»:
history.replaceState(
{ route: "/products" },
"",
"/products"
);Наприклад, після першого завантаження сторінки можна ініціалізувати стан:
if (history.state === null) {
history.replaceState(
{ route: location.pathname },
"",
location.href
);
}popstateПодія popstate спрацьовує, коли активним стає інший запис історії, наприклад після:
натискання кнопки «Назад»;
натискання кнопки «Вперед»;
виклику history.back();
виклику history.forward();
виклику history.go().
window.addEventListener("popstate", (event) => {
console.log("Поточний URL:", location.href);
console.log("Стан запису:", event.state);
});Важливо розрізняти ці два сценарії:
history.pushState({ page: "about" }, "", "/about");
// popstate НЕ спрацьовує автоматичноhistory.back();
// після переходу браузер викличе popstateТому зручно мати одну функцію відображення сторінки й викликати її:
після початкового завантаження;
після pushState();
у слухачі popstate.
Маршрутизатор — це код, який визначає, що показати для поточного URL.
Наприклад, для таких маршрутів:
/
/about
/products
/products/42можна перевіряти location.pathname:
function renderRoute() {
const path = location.pathname;
if (path === "/") {
renderHome();
} else if (path === "/about") {
renderAbout();
} else if (path === "/products") {
renderProducts();
} else {
renderNotFound();
}
}Для складніших маршрутів можна використовувати регулярні вирази:
const productMatch = location.pathname.match(/^\/products\/(\d+)$/);
if (productMatch) {
const productId = productMatch[1];
renderProduct(productId);
}Щоб посилання не перезавантажували сторінку, можна перехопити подію click.
document.addEventListener("click", (event) => {
const link = event.target.closest("a");
if (!link) {
return;
}
const url = new URL(link.href);
if (url.origin !== location.origin) {
return;
}
event.preventDefault();
history.pushState(
{ route: url.pathname },
"",
url.pathname
);
renderRoute();
});Перевірка url.origin важлива: внутрішню навігацію можна обробляти власноруч, а зовнішні посилання краще залишати браузеру.
У реальному застосунку також потрібно врахувати:
натискання середньою кнопкою миші;
Ctrl або Cmd під час натискання;
атрибут target;
завантаження файлів;
посилання з download.
Наприклад:
document.addEventListener("click", (event) => {
const link = event.target.closest("a");
if (
!link ||
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey ||
link.target === "_blank" ||
link.hasAttribute("download")
) {
return;
}
const url = new URL(link.href);
if (url.origin !== location.origin) {
return;
}
event.preventDefault();
history.pushState({ route: url.pathname }, "", url.href);
renderRoute();
});Нижче наведено невеликий застосунок без сторонніх бібліотек. Він:
має кілька маршрутів;
змінює URL без перезавантаження;
обробляє кнопки «Назад» і «Вперед»;
демонструє параметр маршруту /products/:id;
використовує replaceState() для початкової ініціалізації.
<!doctype html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<title>History API</title>
<style>
body {
font-family: sans-serif;
max-width: 700px;
margin: 40px auto;
padding: 0 16px;
}
nav {
display: flex;
gap: 12px;
margin-bottom: 24px;
}
nav a {
color: #06c;
}
.product {
padding: 12px;
border: 1px solid #ccc;
margin: 8px 0;
}
button {
margin-right: 8px;
}
</style>
</head>
<body>
<nav>
<a href="/">Головна</a>
<a href="/products">Товари</a>
<a href="/about">Про застосунок</a>
</nav>
<main id="app"></main>
<script>
const app = document.querySelector("#app");
const products = [
{ id: 1, name: "Клавіатура", price: 2500 },
{ id: 2, name: "Миша", price: 1200 },
{ id: 3, name: "Монітор", price: 9800 }
];
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function renderHome() {
app.innerHTML = `
<h1>Головна</h1>
<p>Ця сторінка відображена без повного перезавантаження документа.</p>
`;
}
function renderAbout() {
app.innerHTML = `
<h1>Про застосунок</h1>
<p>Навігація працює через History API.</p>
`;
}
function renderProducts() {
const items = products.map((product) => `
<li class="product">
<a href="/products/${product.id}">
${escapeHtml(product.name)}
</a>
— ${product.price} грн
</li>
`).join("");
app.innerHTML = `
<h1>Товари</h1>
<ul>${items}</ul>
`;
}
function renderProduct(productId) {
const product = products.find((item) => item.id === productId);
if (!product) {
renderNotFound();
return;
}
app.innerHTML = `
<h1>${escapeHtml(product.name)}</h1>
<p>Ціна: ${product.price} грн</p>
<p>
<a href="/products">Повернутися до списку товарів</a>
</p>
`;
}
function renderNotFound() {
app.innerHTML = `
<h1>404</h1>
<p>Сторінку не знайдено.</p>
`;
}
function renderRoute() {
const path = location.pathname;
if (path === "/") {
renderHome();
return;
}
if (path === "/about") {
renderAbout();
return;
}
if (path === "/products") {
renderProducts();
return;
}
const productMatch = path.match(/^\/products\/(\d+)$/);
if (productMatch) {
const productId = Number(productMatch[1]);
renderProduct(productId);
return;
}
renderNotFound();
}
function navigate(url) {
const nextUrl = new URL(url, location.origin);
if (nextUrl.origin !== location.origin) {
return;
}
if (
nextUrl.pathname === location.pathname &&
nextUrl.search === location.search &&
nextUrl.hash === location.hash
) {
return;
}
const route = nextUrl.pathname;
history.pushState(
{ route },
"",
nextUrl.href
);
renderRoute();
}
document.addEventListener("click", (event) => {
const link = event.target.closest("a");
if (
!link ||
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey ||
link.target === "_blank" ||
link.hasAttribute("download")
) {
return;
}
const url = new URL(link.href);
if (url.origin !== location.origin) {
return;
}
event.preventDefault();
navigate(url.href);
});
window.addEventListener("popstate", (event) => {
console.log("Відновлено стан:", event.state);
renderRoute();
});
if (history.state === null) {
history.replaceState(
{ route: location.pathname },
"",
location.href
);
}
renderRoute();
</script>
</body>
</html>Цей файл можна відкрити через локальний HTTP-сервер. Для коректної роботи прямий перехід на вкладений маршрут, наприклад /products/2, сервер має спрямовувати до основного HTML-документа. Інакше сервер може повернути власну помилку 404 ще до запуску JavaScript.
URLSearchParamsURL складається не лише зі шляху. Наприклад:
/products?page=2&sort=priceПараметри можна читати через URLSearchParams:
const params = new URLSearchParams(location.search);
const page = params.get("page");
const sort = params.get("sort");
console.log(page); // "2"
console.log(sort); // "price"Щоб змінити параметр без повного перезавантаження:
const params = new URLSearchParams(location.search);
params.set("page", "3");
const url = `${location.pathname}?${params.toString()}`;
history.pushState(
{ page: 3 },
"",
url
);
renderRoute();Якщо зміна параметра не повинна створювати новий крок у навігації, використовуйте replaceState():
const url = `${location.pathname}?view=compact`;
history.replaceState(
{ view: "compact" },
"",
url
);URL має описувати стан, до якого користувач може повернутися або яким може поділитися.
Наприклад, краще:
/products/42?tab=reviewsніж зберігати всю інформацію лише в JavaScript:
currentProductId = 42;
currentTab = "reviews";Якщо стан є в URL, користувач може:
оновити сторінку;
скопіювати адресу;
відкрити її в іншій вкладці;
скористатися кнопками «Назад» і «Вперед».
Водночас у history.state можна зберігати додаткові службові дані:
history.pushState(
{
route: "/products/42",
scrollY: window.scrollY
},
"",
"/products/42"
);Не варто покладатися лише на history.state: після повного перезавантаження сторінки JavaScript має вміти відновити екран із URL.
hash і History APIФрагмент URL, або hash, розташований після символу #:
/docs#installationПерехід між hash-значеннями викликає подію hashchange:
window.addEventListener("hashchange", () => {
console.log(location.hash);
});Hash-навігація має іншу модель:
частина після # не надсилається серверу;
браузер часто прокручує сторінку до елемента з відповідним id;
для hash використовується hashchange, а не popstate.
History API зазвичай використовують для маршрутів застосунку, а hash — для якорів усередині сторінки або простих застосунків, яким не потрібні повноцінні URL-маршрути.
Історією можна керувати програмно:
history.back();
history.forward();
history.go(-2);
history.go(1);Перевірити кількість записів можна через:
console.log(history.length);Однак немає надійного способу прочитати довільні URL з історії користувача. Браузер навмисно обмежує такий доступ із міркувань приватності.
Після переходу між маршрутами браузер може зберігати позицію прокручування. Для SPA це не завжди бажано: новий маршрут часто має відкриватися з початку.
Простий варіант:
function navigate(url) {
history.pushState({}, "", url);
window.scrollTo(0, 0);
renderRoute();
}Для складнішої поведінки позицію прокручування можна зберігати в history.state:
history.replaceState(
{
...history.state,
scrollY: window.scrollY
},
"",
location.href
);Під час popstate:
window.addEventListener("popstate", (event) => {
renderRoute();
const scrollY = event.state?.scrollY ?? 0;
window.scrollTo(0, scrollY);
});Реалізацію потрібно узгодити з логікою застосунку: для переходу на новий маршрут зазвичай прокручують сторінку вгору, а під час повернення назад — відновлюють попередню позицію.
Маршрут і завантаження даних — це різні операції. Спочатку URL можна змінити, потім показати стан завантаження, а після завершення запиту оновити інтерфейс.
async function renderProductPage(productId) {
app.innerHTML = "<p>Завантаження...</p>";
try {
const response = await fetch(`/api/products/${productId}`);
if (!response.ok) {
throw new Error("Не вдалося завантажити товар");
}
const product = await response.json();
app.innerHTML = `
<h1>${escapeHtml(product.name)}</h1>
<p>${escapeHtml(product.description)}</p>
`;
} catch (error) {
app.innerHTML = `
<p role="alert">
Не вдалося завантажити дані.
</p>
`;
}
}Якщо користувач швидко переходить між маршрутами, старий запит може завершитися після нового. У складних застосунках для цього використовують AbortController або перевіряють, чи актуальний ще поточний маршрут.
pushState() змінює запис історії, але не завантажує сторінку:
history.pushState({}, "", "/about");Він не:
виконує новий HTTP-запит;
запускає обробку HTML сервером;
автоматично змінює DOM;
викликає popstate.
Тому застосунок сам відповідає за:
визначення нового маршруту;
завантаження необхідних даних;
оновлення DOM;
показ стану помилки;
синхронізацію інтерфейсу з кнопками «Назад» і «Вперед».
Перехоплення посилань не повинно ламати стандартну поведінку браузера.
Потрібно:
використовувати справжні елементи <a href="...">;
не замінювати посилання кнопками без необхідності;
не перехоплювати Ctrl/Cmd-клік;
не заважати відкриттю посилання в новій вкладці;
після зміни маршруту за потреби переводити фокус на заголовок або основний контейнер.
Наприклад:
function focusMainHeading() {
const heading = document.querySelector("main h1");
if (!heading) {
return;
}
heading.tabIndex = -1;
heading.focus();
}Після renderRoute() можна викликати focusMainHeading(), щоб користувачі клавіатури та допоміжних технологій розуміли, що вміст змінився.
Під час переходу за внутрішнім посиланням зазвичай відбувається така послідовність:
Користувач натискає посилання.
Обробник перевіряє, чи є посилання внутрішнім.
Стандартне завантаження документа скасовується через preventDefault().
Викликається history.pushState().
Застосунок читає location.pathname і location.search.
Відображається потрібний екран.
Під час натискання «Назад» або «Вперед» спрацьовує popstate.
Застосунок знову відображає маршрут із поточного URL.
pushState() викличе popstateЦе не відбувається автоматично:
history.pushState({}, "", "/about");
// popstate не викликаноПісля pushState() потрібно самостійно викликати функцію рендерингу.
popstateЯкщо обробити лише натискання посилань, кнопки «Назад» і «Вперед» змінюватимуть URL, але інтерфейс залишатиметься старим.
window.addEventListener("popstate", renderRoute);pushState() замість replaceState()Якщо кожне виправлення URL додає новий запис, користувачеві доведеться натискати «Назад» кілька разів, щоб повернутися до попереднього екрана.
Для заміни поточного запису використовуйте replaceState().
history.statehistory.state не замінює централізоване сховище або URL. Не зберігайте там великі об’єкти й дані, які можна відновити з адреси або сервера.
Перевірка лише location.pathname ігнорує:
/products?page=2Якщо параметри впливають на екран, потрібно читати location.search через URLSearchParams.
Не можна обробляти всі <a> однаково. Посилання на інший домен, посилання для нової вкладки та завантаження файлу мають зберігати стандартну поведінку.
innerHTMLДані від сервера або з URL не слід безпосередньо вставляти в innerHTML. Це може створити XSS-вразливість.
Замість цього:
екрануйте текст;
використовуйте textContent;
обережно формуйте HTML-шаблони;
перевіряйте дані перед відображенням.
Після оновлення сторінки на маршруті /products/42 сервер має повернути основний HTML-документ застосунку. History API не налаштовує серверну маршрутизацію автоматично.
history.pushState() додає запис в історію без перезавантаження сторінки.
history.replaceState() замінює поточний запис.
popstate спрацьовує під час переходів між записами історії.
pushState() не викликає popstate автоматично.
Застосунок має самостійно синхронізувати URL і DOM.
Для маршрутизації потрібно обробляти pathname, а за потреби — search і hash.
Внутрішні посилання можна перехоплювати, але не слід ламати стандартну поведінку браузера.
URL має містити стан, який потрібно відновити після оновлення сторінки.
Для повноцінної роботи History API потрібна серверна конфігурація, що повертає застосунок для відомих клієнтських маршрутів.