はじめに
svelteを使っていて、popstateの発火時に、$page.url.href
とwindow.location.href
で取得される値が異なっていて困ったので調べてみました。
環境
以下のようなコードを用意します。
<script lang="ts">
import { page } from "$app/stores";
let uid = 0;
let state = {};
function pushState() {
uid += 1;
history.pushState(uid, "", `/${uid}`);
}
function popState(event: PopStateEvent) {
console.log($page.url.href, window.location.href);
state = event.state;
}
</script>
<svelte:window on:popstate={popState} />
<button on:click={pushState}>click me</button>
<pre>{JSON.stringify(state)}</pre>
このコードは以下URL内の改変です。
https://svelte.dev/repl/ecfaef5f5e5f02c2ebf160b4ce06ac0a?version=4.2.9
click me
を押した後、この中のconsole.log
の結果が異なります。
例えば、click me
を2回押したとき、{{url}}/2
になります。
その後、ブラウザの戻るボタンを押した時、$page.url.href
の結果は{{url}}/2
ですが、window.location.href
の結果は{{url}}/1
になります。
調べた結果
そもそも、popstateの発火時として、document はまだ反映されていないかもしれないことを考慮に入れておくことが重要です
とあります。
When writing functions that process popstate event it is important to take into account that properties like window.location will already reflect the state change (if it affected the current URL), but document might still not. If the goal is to catch the moment when the new document state is already fully in place, a zero-delay setTimeout() method call should be used to effectively put its inner callback function that does the processing at the end of the browser event loop: window.onpopstate = () => setTimeout(doSomeThing, 0);
popstate イベントを処理する関数を書くときには、 window.location のようなプロパティはすでに状態の変化を反映していますが(それが現在の URL に影響する場合)、 document はまだ反映されていないかもしれないことを考慮に入れておくことが重要です。新しい文書の状態が完全に反映された瞬間を捉えることが目的であれば、遅延ゼロの setTimeout() メソッド呼び出しを使用して、処理を行う内部の callback 関数をブラウザーのイベントループの最後に効果的に配置する必要があります。例えば window.onpopstate = () => setTimeout(doSomeThing, 0); のようにします。
ただ、今回の例であればconsole.log
のタイミングでdocument.URL
を見ても、window.location.href
と同じ値が返ってくると思います。
また、その続きである新しい文書の状態が完全に反映された瞬間を捉えることが目的であれば ~~~
の部分の通り、console.log
の後にsetTimeout(() => console.log($page.url.href), 0);
を入れてみると、window.location.href
と同じ値になります。
ちなみに、$page.url
の変更タイミングは恐らくここになります。
https://github.com/sveltejs/kit/blob/99612ace885a6085b87e1ee1f38b0eea9ae2ba14/packages/kit/src/runtime/client/client.js#L2172-L2173
上の方を見ていくと、addEventListener('popstate'
とあるため、popstateの発火タイミングのずれによって変更されてないため、値が取れないということが分かります(試しにlog埋め込んでみると分かります)
まとめ
どうも似たような質問も飛んでました。
https://www.reddit.com/r/sveltejs/comments/14frcs4/browsers_popstate_event_handling/
解決方法としてはsetTimeout
を使ってでも$app/stores
のpage
を用いるか、ここはいっそwindow.location.href
の二択になると思います。
もし他に良案があれば教えてください。