fix(tradein/v2): гонка при клике по строке "Предыдущие оценки" — оценка не загружалась
router.replace(`/v2?id=`) — асинхронный soft-nav, а setNav(0) в том же тике вызывает синхронный re-render ДО того как window.location реально обновится → readUrlId() читает устаревшее значение навсегда (никто больше не форсит re-render). Добавлен pendingRestoreId — синхронный override, аналогично тому как freshResult уже обходит эту же гонку для post-submit пути.
This commit is contained in:
parent
3a2b4ba825
commit
535c2d3855
1 changed files with 52 additions and 1 deletions
|
|
@ -401,8 +401,20 @@ export default function TradeInV2Page() {
|
||||||
const [freshResult, setFreshResult] = useState<AggregatedEstimate | null>(
|
const [freshResult, setFreshResult] = useState<AggregatedEstimate | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
// #2424 fix — router.replace(`?id=`) is an async client-navigation (RSC
|
||||||
|
// round-trip) that does NOT update window.location.search synchronously, but
|
||||||
|
// the onSelectEstimate handler below also calls setNav(0) in the same tick,
|
||||||
|
// which DOES force an immediate synchronous re-render. Without an override,
|
||||||
|
// that re-render's readUrlId() call reads the stale (pre-navigation) URL —
|
||||||
|
// and since this page has no other reactive subscription to the URL, the
|
||||||
|
// stale value is locked in forever (mirrors how `freshResult` already
|
||||||
|
// overrides the same race for the post-submit path below). Cleared once
|
||||||
|
// `restored.data` actually reflects this id (see effect below).
|
||||||
|
const [pendingRestoreId, setPendingRestoreId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
const urlId = readUrlId();
|
const urlId = pendingRestoreId ?? readUrlId();
|
||||||
|
|
||||||
// Post-mount flag: readUrlId() is null on SSR but a UUID on the client's first
|
// Post-mount flag: readUrlId() is null on SSR but a UUID on the client's first
|
||||||
// render, so any structure that depends on the id (e.g. the PDF <a> vs disabled
|
// render, so any structure that depends on the id (e.g. the PDF <a> vs disabled
|
||||||
|
|
@ -459,6 +471,29 @@ export default function TradeInV2Page() {
|
||||||
? null
|
? null
|
||||||
: (freshResult?.estimate_id ?? urlId);
|
: (freshResult?.estimate_id ?? urlId);
|
||||||
|
|
||||||
|
// Clear the pendingRestoreId override once BOTH (a) the restore query has
|
||||||
|
// resolved data for that same id AND (b) window.location.search has
|
||||||
|
// genuinely caught up to it — only then is readUrlId() alone guaranteed to
|
||||||
|
// keep returning the right value, so the override is no longer needed.
|
||||||
|
// The (b) check guards a narrow edge case: if the query cache already held
|
||||||
|
// a fresh (within staleTime) response for the clicked id — e.g. it was the
|
||||||
|
// just-submitted/just-restored estimate earlier in the same SPA session —
|
||||||
|
// `restored.data` can resolve synchronously, before router.replace's async
|
||||||
|
// navigation has actually updated the URL. Clearing on (a) alone would then
|
||||||
|
// drop the override a beat too early and briefly re-expose the very race
|
||||||
|
// this state exists to prevent. If (b) isn't true yet, we simply leave the
|
||||||
|
// (still-correct) override in place — harmless, since it already matches
|
||||||
|
// what the URL will become — rather than risk reverting to a stale read.
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
pendingRestoreId !== null &&
|
||||||
|
restored.data?.estimate_id === pendingRestoreId &&
|
||||||
|
readUrlId() === pendingRestoreId
|
||||||
|
) {
|
||||||
|
setPendingRestoreId(null);
|
||||||
|
}
|
||||||
|
}, [pendingRestoreId, restored.data]);
|
||||||
|
|
||||||
// Per-user side data, independent of the current estimate. Both fail-soft
|
// Per-user side data, independent of the current estimate. Both fail-soft
|
||||||
// (retry:false): history feeds the «Предыдущие оценки» overlay (04), quota
|
// (retry:false): history feeds the «Предыдущие оценки» overlay (04), quota
|
||||||
// feeds the TopNav «Мои отчёты» badge. Refreshed after a successful estimate
|
// feeds the TopNav «Мои отчёты» badge. Refreshed after a successful estimate
|
||||||
|
|
@ -637,6 +672,9 @@ export default function TradeInV2Page() {
|
||||||
mutation.mutate(input, {
|
mutation.mutate(input, {
|
||||||
onSuccess: (est) => {
|
onSuccess: (est) => {
|
||||||
setFreshResult(est);
|
setFreshResult(est);
|
||||||
|
// A fresh submission always supersedes any pending restore-by-id
|
||||||
|
// override (#2424 fix) — drop it so it can't shadow a later restore.
|
||||||
|
setPendingRestoreId(null);
|
||||||
// A new estimate consumed quota and added a history row — refresh both
|
// A new estimate consumed quota and added a history row — refresh both
|
||||||
// so the TopNav badge + «Предыдущие оценки» overlay reflect it.
|
// so the TopNav badge + «Предыдущие оценки» overlay reflect it.
|
||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
|
|
@ -925,6 +963,19 @@ export default function TradeInV2Page() {
|
||||||
// above (line ~649); this file uses replace() consistently for
|
// above (line ~649); this file uses replace() consistently for
|
||||||
// every /v2?id= transition, so we follow that convention here
|
// every /v2?id= transition, so we follow that convention here
|
||||||
// too rather than introducing the file's only push() call.
|
// too rather than introducing the file's only push() call.
|
||||||
|
//
|
||||||
|
// #2424 fix: router.replace() is an async client-navigation —
|
||||||
|
// window.location.search does NOT update synchronously — but
|
||||||
|
// setNav(0) below DOES force an immediate synchronous
|
||||||
|
// re-render in the same tick. Without pendingRestoreId,
|
||||||
|
// readUrlId() on that re-render would read the stale
|
||||||
|
// (pre-navigation) URL and — since this page has no other
|
||||||
|
// reactive subscription to the URL — that stale value would
|
||||||
|
// be locked in forever. setPendingRestoreId synchronously
|
||||||
|
// hands `urlId` the correct target id, mirroring how
|
||||||
|
// `freshResult` already overrides this same race for the
|
||||||
|
// post-submit path (handleSubmit above).
|
||||||
|
setPendingRestoreId(id);
|
||||||
router.replace(`/v2?id=${id}`, { scroll: false });
|
router.replace(`/v2?id=${id}`, { scroll: false });
|
||||||
setNav(0);
|
setNav(0);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue