popstateが発火しない?pushStateが必要な理由や画面クリックが必要な理由を解説!

「戻るボタンを押してもpopstateイベントが発火しないんだけど…?」JavaScriptで戻るボタンの挙動を制御しようとしたとき、こんな疑問にぶつかったことはありませんか?

この記事では、JavaScriptのpopstateイベントが発火しない原因と対策について、図やサンプルコードを用いてわかりやすく解説します。

window.addEventListener('popstate', ...)を書いたのに反応しない!」、「pushStateを使わないとダメなの?」こんな疑問を持った方は、ぜひ読んでみてください!

popstateとは

popstateは、「ブラウザの戻る・進む操作」や「history.back()/history.forward()」などによって履歴が移動したときに発火するイベントです。たとえば、以下のように記述すると、履歴が移動したタイミングでイベントが発火します。

window.addEventListener('popstate', (event) => {
  console.log('popstate 発火!', event.state);
});

popstateは、以下のような「履歴の移動(戻る/進む)」のときにのみ発火する点に注意してください。

  • history.pushState()によって追加された履歴が「戻された時
  • history.pushState()によって追加された履歴に「進んだ時

そのため、普通のページ遷移やリンククリックではpopstateは発火しません。この理由についてこれから詳しく説明します。

popstateが発火しない理由

理由1:pushStateをしていないから

popstateは、history.pushState()によって履歴を追加した後、追加された履歴が「戻された時」や追加された履歴に「進んだ時」にしか発火しません。たとえば、以下のようにhistory.pushState()で履歴を1つ追加した後、ユーザーが戻るボタンを押すとpopstateが発火します。

history.pushState(null, null, '/page1');
window.addEventListener('popstate', (event) => {
  console.log('popstate 発火!', event.state);
});

逆に、history.pushState()をしていない状態では、戻る操作があってもpopstateは発火しません。history.pushState()していない状態している状態で動作の違いを図を用いてもう少し詳しく説明します。

history.pushState()をしていない状態

例えば、http://a.comからhttp://b.comにページ遷移したとします。この状態において、ブラウザの「戻る」ボタンを押してブラウザバッグをしても、popstateは発火しません。なぜなら、ブラウザにとっては「history.pushState()を使って追加された履歴」がない状態なので、戻るボタンを押しても、popstateが発火しないのです。

popstateが発火しない理由(history.pushStateをしていない状態)

history.pushState()をしている状態

今度は、http://a.comからhttp://b.comにページ遷移し、http://b.comのJavaScriptでhistory.pushState()を実行して履歴を追加した場合を考えます。このとき、履歴やアドレスバーの値は「history.pushState()で指定したURL」に切り替わっていますが、historyAPIを使って履歴を変更した場合、画面ロードが走らないため、実際の画面表示は変わっていません。画面上はhistory.pushState()実行前の「B画面」が表示されています。

history.pushStateの挙動

  • 変化するもの: 履歴、アドレスバーの値
  • 変化しないもの: 表示されている画面

この状態において、ブラウザの「戻る」ボタンを押してブラウザバッグすると、「history.pushState()を使って追加された履歴」が戻されているのでpopstateが発火します。

popstateが発火しない理由(history.pushStateをしている状態)

理由2:replaceStateだけ使っているから

history.replaceState()は、現在の履歴エントリを「上書き」するだけなので、戻る履歴が増えません。そのため、以下のコードでは戻るボタンをクリックしてもpopstateは発火しません。

history.replaceState({ page: 'page1' }, '', '/page1');
window.addEventListener('popstate', (event) => {
  console.log('popstate 発火!', event.state);
});

だだし、すでにhistory.pushState()を使って履歴が増えていて、そのあとにhistory.replaceState()を使った場合は、前の履歴に戻るときにpopstateが発火します。例えば、以下のコードでは、history.pushState()したあとにhistory.replaceState()を使っています。

// 初期URL: /home
history.pushState({ page: 'page1' }, '', '/page1');      // /page1 を追加
history.replaceState({ page: 'page1b' }, '', '/page1b');  // /page1 を上書き

// 履歴は:[/home] → [/page1b]

window.addEventListener('popstate', (event) => {
  console.log('popstate 発火!', event.state);
});

この状態で戻るボタンを押すと、history.pushState()によって追加された履歴が戻されているのでpopstateが発火します。なお、event.stateには/homeに対応するstate(null)が入ります。

理由3: ユーザー操作(クリックなど)がないとpopstateが発火しない場合があるから

現在のChromeやEdgeでは、セキュリティやユーザー保護の観点から、ユーザーが一度画面をクリックするなどの操作(インタラクション)を行わない限り、popstateイベントが発火しない仕様になっています。

そのため、ページを開いてすぐに「Webブラウザの戻るボタン」や「バッグスペースキー」を押しても、JavaScriptでの制御が効かず、ブラウザバックを無効化できません。

Firefoxではページを開いた直後でもpopstateイベントが発火するため、ブラウザバックの無効化が機能します(2023年12月時点の情報)。ただし今後、FirefoxもChromeやEdgeと同じ仕様に合わせてくる可能性がありますので注意してください。

本記事のまとめ

この記事では『popstateイベントが発火しない原因と対策』について、以下の内容を説明しました。

  • popstateとは
  • popstateが発火しない理由
    • 理由1:pushStateをしていないから
    • 理由2:replaceStateだけ使っているから
    • 理由3: ユーザー操作(クリックなど)がないとpopstateが発火しない場合があるから

お読み頂きありがとうございました。