Fixes the 5 findings from /code-review high on the account-pin work: - Extract the reactive-account primitives (ACTIVE_ACCOUNT_KEY, activeAccountId, notifyActiveAccountChange, useActiveAccountId) into a new dependency-free lib/activeAccount.ts, breaking the storage.ts <-> useAccountRehydrate.ts import cycle. storage.ts / api/core.ts / useAccountRehydrate.ts now import from it. - NavigationProvider: the page re-hydrate now PRESERVES existing history.state markers (spread, not overwrite), and openChannel/enterYtSearch markDirty so a sub-view opened before the account pins isn't clobbered by the page override. - useAccountRehydrate: reset the dirty flag after each account transition, so a future non-reloading account switch can't skip the re-hydrate and leak the prior account's state. - Doc-link useAccountRehydrate to the pre-existing useEffect([me?.id]) server-adopt pattern. - Tests: model a mutable pinned account and add useSortState integration tests for the null->id re-hydrate + the markDirty no-clobber guard (vitest 115->117). gate-green: vite build + vitest 117 + eslint-0 + knip + prettier. Self-E2E'd on a fresh tab: still lands on Plex with Movies/In-progress/Year restored, zero console errors.
40 lines
2.1 KiB
TypeScript
40 lines
2.1 KiB
TypeScript
import { useCallback, useEffect, useRef } from "react";
|
|
import { useActiveAccountId } from "./activeAccount";
|
|
|
|
// Re-read per-account persisted state when this tab's active account becomes known (or changes). The
|
|
// provider tree above <App> mounts before the account is pinned, so its useState initializers seed from
|
|
// an unknown account (empty defaults); once <App> pins it, `rehydrate` runs to load the real stored
|
|
// value. Guarded so it never clobbers a value the user set in the brief pre-pin window: the returned
|
|
// `markDirty` is called by the owning setter, and a dirty state skips the re-hydrate.
|
|
//
|
|
// Keyed on the account id ONLY — the user's own writes go through setAccountRaw, which does not change
|
|
// the account id, so this effect can't loop on them.
|
|
//
|
|
// This is the localStorage-only counterpart of the `useEffect([me?.id])` server-prefs adopt pattern in
|
|
// FeedFiltersProvider / FeedViewProvider / PrefsProvider (and App's shell prefs): those re-load from the
|
|
// SERVER when the account query resolves; this re-loads from localStorage when the account is pinned.
|
|
// A provider needs one or the other — not both — to survive a fresh tab's first paint.
|
|
export function useAccountRehydrate(rehydrate: () => void): () => void {
|
|
const accountId = useActiveAccountId();
|
|
const dirty = useRef(false);
|
|
const prev = useRef(accountId);
|
|
// Latest closure in a ref so the effect depends on the account id alone (a fresh `rehydrate` each
|
|
// render must not re-fire it), while still calling the current logic.
|
|
const fn = useRef(rehydrate);
|
|
fn.current = rehydrate;
|
|
|
|
useEffect(() => {
|
|
if (accountId === prev.current) return; // mount, and renders with no real transition
|
|
prev.current = accountId;
|
|
if (!dirty.current) fn.current();
|
|
// Reset per account: `dirty` means "the user edited under THIS account". Today a real account
|
|
// switch full-reloads (fresh instance), but clearing it here keeps the guard correct even if a
|
|
// future switch stops reloading — the next account starts clean rather than inheriting dirtiness.
|
|
dirty.current = false;
|
|
}, [accountId]);
|
|
|
|
return useCallback(() => {
|
|
dirty.current = true;
|
|
}, []);
|
|
}
|