-
Notifications
You must be signed in to change notification settings - Fork 478
historyPoppedWithEmptyState triggers restore visits on pages with data-turbo="false" #1496
Description
Summary
Turbo 8.0.21 introduced historyPoppedWithEmptyState (from PR #1285 — "Simplified same-page anchor visits"). This new handler injects Turbo state into history entries that were created by non-Turbo hash-based routing. On subsequent back/forward navigation to those entries, Turbo performs a full restore visit (server fetch + DOM replacement), even on pages with data-turbo="false".
Steps to reproduce
- Have a page with
data-turbo="false"on the<body>and<meta name="turbo-cache-control" content="no-cache"> - Use hash-based routing to navigate between views on the page — e.g.
#step1→#step2→#step3 - Press the browser back button (or call
history.back())
Expected behavior
Turbo should not interfere with navigation on pages that have data-turbo="false". Hash-based routing should work independently of Turbo, as it did in 8.0.20.
Actual behavior (8.0.21)
When popstate fires for a hash-only history entry (no Turbo state in event.state), the new else branch in onPopState calls historyPoppedWithEmptyState, which:
- Calls
history.replaceState()to inject Turbo state into the entry - Sets
this.view.lastRenderedLocation - Calls
this.view.cacheSnapshot()
On a subsequent popstate to that same entry (e.g. navigating back again after the hash router re-navigates forward), Turbo now sees turbo state and calls historyPoppedToLocationWithRestorationIdentifierAndDirection, which starts a restore visit — regardless of data-turbo="false" on the body. Since Session.enabled is always true (it's independent of data-turbo), the visit proceeds, fetching the page from the server and replacing the entire DOM.
Example repro
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="turbo-cache-control" content="no-cache">
<title>Turbo 8.0.21 popstate bug repro</title>
<script type="module">
import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.21/dist/turbo.es2017-esm.js';
// Log all popstate events
window.addEventListener('popstate', (e) => {
log(`popstate fired — state: ${JSON.stringify(e.state)}, hash: ${location.hash}`);
}, true); // capture phase, runs before Turbo
// Log Turbo visits (the bug: restore visit on data-turbo="false" page)
document.addEventListener('turbo:before-visit', (e) => {
log(`turbo:before-visit — url: ${e.detail.url}`);
});
document.addEventListener('turbo:visit', (e) => {
log(`turbo:visit — url: ${e.detail.url}, action: ${e.detail.action}`);
});
document.addEventListener('turbo:load', () => {
log('turbo:load — PAGE WAS REPLACED BY TURBO');
});
document.addEventListener('turbo:before-render', () => {
log('turbo:before-render — TURBO IS REPLACING THE DOM');
});
function log(msg) {
const el = document.getElementById('log');
if (el) {
el.textContent += `[${new Date().toISOString().slice(11,23)}] ${msg}\n`;
}
console.log(msg);
}
window.log = log;
// Simple hash-based navigation (like routie, defined inline for simplicity)
window.navigateHash = function(hash) {
log(`navigateHash("${hash}") — setting window.location.hash`);
window.location.hash = hash;
};
</script>
</head>
<body data-turbo="false">
<h1>Turbo 8.0.21 — <code>data-turbo="false"</code> + hash routing bug</h1>
<p>
This page has <code>data-turbo="false"</code> on the body and uses
hash-based routing (like <code>routie</code>). Turbo should not
interfere with navigation, but 8.0.21's new <code>historyPoppedWithEmptyState</code>
injects Turbo state into hash-only history entries.
</p>
<h2>Steps to reproduce</h2>
<ol>
<li><button onclick="navigateHash('step1')">Navigate to #step1</button></li>
<li><button onclick="navigateHash('step2')">Navigate to #step2</button></li>
<li><button onclick="log('--- Calling history.back() ---'); history.back()">history.back()</button>
— Turbo injects state into the #step1 entry</li>
<li><button onclick="log('--- Calling history.back() again ---'); history.back()">history.back() again</button>
— Turbo sees the injected state and starts a <strong>restore visit</strong>,
fetching the page from the server and replacing the DOM</li>
</ol>
<p>Current hash: <strong id="current-hash"></strong></p>
<script>
function updateHash() {
document.getElementById('current-hash').textContent = location.hash || '(none)';
}
window.addEventListener('hashchange', updateHash);
updateHash();
</script>
<h2>Event log</h2>
<pre id="log" style="background:#f0f0f0; padding:1em; max-height:400px; overflow:auto;"></pre>
</body>
</html>