fix(nav): the address follows the view through overlays, reloads and Back (review round 1)
- A URL mirror names the view again on every entry Back or Forward lands on, so a change made under a dialog, a drawer or a sub-view is not undone by the next reload. - A view pushed from the entry a reload under a dialog started from no longer inherits its overlay mark; a reload under two overlays closes onto the page in one step. - The linked player reopens over a live search, gives itself up when its list moves first, and plays alone over an emptied playlist; a capped Files list keeps the link it cannot resolve. - The Feed press over a channel ends the search; leaving a search ends its asked mark; Files and Playlists searches end with their page; the feed always names its view and debounces its search. - Search-box keys are declared per module and kept out of the client log below TRACE. - Writers outside lib/ go through codecs; the URL contract scans computed keys, overlay writers and two-line reads, and holds every page to a bookmark round trip (Config, Activity, Playlists and the Files sort and viewer added).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useEffectEvent } from "react";
|
||||
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
|
||||
@@ -6,17 +6,44 @@ import { urlWithParams } from "./urlState";
|
||||
// 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<string, string | null>): 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 one reactive input is the
|
||||
// serialized value — which is exactly what the dep list says: an effect event for the identity that
|
||||
// churns, a primitive for the trigger.
|
||||
// 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(() => {
|
||||
window.history.replaceState(window.history.state, "", urlWithParams(updates));
|
||||
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]);
|
||||
}, [dep, landings]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user