import { useEffect, useEffectEvent, useState } from "react"; import { urlWithParams } from "./urlState"; // Mirror a set of nav params onto the CURRENT history entry (replaceState — mirroring is not a new // entry) whenever they change. Each provider owns its own key(s) and passes the delta it wants; a null // value clears that key, and urlWithParams preserves every other param (so providers don't clobber each // other). Extracted from the identical replaceState/urlWithParams blocks FeedFiltersProvider and // PlaylistsProvider used to hand-roll, so the mirroring contract lives in one place. // // Also whenever Back/Forward makes another entry current: the browser restores that entry's address, // which says what the view was when it was left — a change made since on an entry above it (a dialog's // or a drawer's, a sub-view's) is not in it, and a reload would bring the old view back over the one // on screen. (One landed on by an overlay's step back that another step will leave is written too, // and harmlessly: the next landing writes the entry that stays.) export function useUrlMirror(updates: Record): void { // The caller typically passes an inline object literal (new identity every render); key on its // serialized value so the effect only fires when a param actually changes, not every render. const dep = JSON.stringify(updates); // The object itself is read through an effect event, so the effect's reactive inputs are the // serialized value and the landing count — which is exactly what the dep list says: an effect event // for the identity that churns, primitives for the triggers. const mirror = useEffectEvent(() => { const href = urlWithParams(updates); // Every mirror runs on every landing: one with nothing to change writes nothing (a browser // throttles a page that rewrites its address too often). if (href !== urlWithParams({})) window.history.replaceState(window.history.state, "", href); }); // Counted in state, on a task after the popstate dispatch: every listener of it has run by then — // a store that reads its keys back from the landed address among them (Activity) — and so has the // render each one caused (React renders a popstate's updates at once, between listeners). A write // inside the dispatch put the store's old value over the address a later listener was still to // read. const [landings, setLandings] = useState(0); useEffect(() => { let timer = 0; const onPop = () => { window.clearTimeout(timer); timer = window.setTimeout(() => setLandings((n) => n + 1), 0); }; window.addEventListener("popstate", onPop); return () => { window.removeEventListener("popstate", onPop); window.clearTimeout(timer); }; }, []); useEffect(() => { mirror(); }, [dep, landings]); }