fix(tradein/v2): 3-state pendingRestoreId sentinel — null и undefined разные вещи
Живая проверка в браузере (клик по строке "Предыдущие оценки" на проде)
подтвердила: router.replace() — асинхронный client-nav (RSC round-trip),
window.location.search не обновляется синхронно, а readUrlId() читает
window.location.search напрямую (useSearchParams намеренно не используется —
ломает next build через Suspense). Единственный надёжный источник urlId
в синхронном ре-рендере — override-state, выставленный в том же тике,
что и клик.
Предыдущая версия (`pendingRestoreId ?? readUrlId()`) использовала null
для двух разных смыслов сразу: "нет override" и "принудительно пусто".
handleNew() выставлял null именно во втором смысле (сброс после 404),
но ?? тут же трактовал null как "нет override" и падал обратно на
readUrlId() — который на тот момент ещё возвращал старый плохой id
(router.replace("/v2") ещё не успел домотать). Итог: "НОВАЯ ОЦЕНКА"
после битой ссылки могла не сбрасывать ошибку.
Три состояния убирают двусмысленность: undefined = нет override (readUrlId
рулит), null = принудительно пусто, string = override на конкретный id.
This commit is contained in:
parent
d3d8d15f67
commit
10f4f6adac
1 changed files with 22 additions and 6 deletions
|
|
@ -410,11 +410,22 @@ export default function TradeInV2Page() {
|
||||||
// stale value is locked in forever (mirrors how `freshResult` already
|
// stale value is locked in forever (mirrors how `freshResult` already
|
||||||
// overrides the same race for the post-submit path below). Cleared once
|
// overrides the same race for the post-submit path below). Cleared once
|
||||||
// `restored.data` actually reflects this id (see effect below).
|
// `restored.data` actually reflects this id (see effect below).
|
||||||
const [pendingRestoreId, setPendingRestoreId] = useState<string | null>(
|
//
|
||||||
null,
|
// Three states, not two: `undefined` means "no override, defer to
|
||||||
);
|
// readUrlId()"; `null` means "override to no-id" (handleNew's reset);
|
||||||
|
// a string means "override to this id" (a row was just clicked). Using
|
||||||
|
// plain `null` for both "no override" and "force no-id" was tried first and
|
||||||
|
// reintroduced this exact race for handleNew(): forcing pendingRestoreId
|
||||||
|
// from a bad id to `null` would, under `pendingRestoreId ?? readUrlId()`,
|
||||||
|
// fall through to readUrlId() on the very next render — which is still
|
||||||
|
// stale until router.replace("/v2") finishes — so the bad id came right
|
||||||
|
// back. The two intents need distinguishable values.
|
||||||
|
const [pendingRestoreId, setPendingRestoreId] = useState<
|
||||||
|
string | null | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const urlId = pendingRestoreId ?? readUrlId();
|
const urlId =
|
||||||
|
pendingRestoreId !== undefined ? 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
|
||||||
|
|
@ -486,11 +497,12 @@ export default function TradeInV2Page() {
|
||||||
// what the URL will become — rather than risk reverting to a stale read.
|
// what the URL will become — rather than risk reverting to a stale read.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
pendingRestoreId !== undefined &&
|
||||||
pendingRestoreId !== null &&
|
pendingRestoreId !== null &&
|
||||||
restored.data?.estimate_id === pendingRestoreId &&
|
restored.data?.estimate_id === pendingRestoreId &&
|
||||||
readUrlId() === pendingRestoreId
|
readUrlId() === pendingRestoreId
|
||||||
) {
|
) {
|
||||||
setPendingRestoreId(null);
|
setPendingRestoreId(undefined);
|
||||||
}
|
}
|
||||||
}, [pendingRestoreId, restored.data]);
|
}, [pendingRestoreId, restored.data]);
|
||||||
|
|
||||||
|
|
@ -674,7 +686,8 @@ export default function TradeInV2Page() {
|
||||||
setFreshResult(est);
|
setFreshResult(est);
|
||||||
// A fresh submission always supersedes any pending restore-by-id
|
// A fresh submission always supersedes any pending restore-by-id
|
||||||
// override (#2424 fix) — drop it so it can't shadow a later restore.
|
// override (#2424 fix) — drop it so it can't shadow a later restore.
|
||||||
setPendingRestoreId(null);
|
// `undefined` (not `null`): there is no override to force, just none.
|
||||||
|
setPendingRestoreId(undefined);
|
||||||
// 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({
|
||||||
|
|
@ -695,6 +708,9 @@ export default function TradeInV2Page() {
|
||||||
|
|
||||||
function handleNew() {
|
function handleNew() {
|
||||||
setFreshResult(null);
|
setFreshResult(null);
|
||||||
|
// `null`, not `undefined`: forces urlId to no-id immediately, synchronously
|
||||||
|
// clearing restoreActive/restoreError even before router.replace below
|
||||||
|
// finishes and readUrlId() would otherwise still read the stale id.
|
||||||
setPendingRestoreId(null);
|
setPendingRestoreId(null);
|
||||||
router.replace("/v2", { scroll: false });
|
router.replace("/v2", { scroll: false });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue