Skip to content

fix: hooks audit round 3 — BUG-08 a BUG-17 (10 bugs corrigidos)#436

Merged
adm01-debug merged 11 commits into
mainfrom
fix/hooks-audit-round3-2026-05
May 26, 2026
Merged

fix: hooks audit round 3 — BUG-08 a BUG-17 (10 bugs corrigidos)#436
adm01-debug merged 11 commits into
mainfrom
fix/hooks-audit-round3-2026-05

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented May 26, 2026

🔍 Terceira Auditoria Exaustiva de Hooks — Maio 2026

Auditoria completa de 378 arquivos de hooks em 21 diretórios de src/hooks/. Encontrados e corrigidos 10 novos bugs.


Bugs Corrigidos

ID Sev Arquivo Problema
BUG-08 🔴 P1 useKitAutoSave.ts Auto-save silenciosamente cancelado por saveToDb nas deps do snapshot effect
BUG-09 🟡 P2 useEntitySelectionMode.ts bulkCartProducts e selectedProducts com cálculo idêntico duplicado
BUG-10 🔴 P1 useWorkspaceNotifications.tsx notifications.length nas deps causa restart do polling a cada leitura
BUG-11 🟠 P2 useGravacaoPriceV2.ts setState após unmount em useCustomizationPriceReactiveLegacy
BUG-12 🟠 P2 useTechniquePricing.ts setState após unmount em fetchPriceOptions
BUG-13 🟡 P3 useKitStockValidation.ts stockByProduct e alerts fora de useMemo (recalculados todo render)
BUG-14 🟡 P2 usePositionHistory.ts pushState usa historyIndex stale em chamadas rápidas consecutivas
BUG-15 🟡 P3 useRecentlyViewed.ts setTimeout não limpo no unmount
BUG-16 🟡 P3 useKitUndoRedo.ts setTimeout não limpo no unmount; delay de 100ms sem gerenciamento
BUG-17 🟠 P2 useGeoBlocking.ts fetchCurrentCountry sem AbortController — setState após unmount

BUG-08 — useKitAutoSave.ts (P1 🔴)

Root cause: saveToDb era recriado a cada mudança de kitState (incluindo totalPrice). Como estava nas deps do snapshot useEffect, o effect re-executava, o cleanup anterior limpava o timer pendente, mas o effect retornava cedo (snapshot igual) sem criar novo timer. Auto-save silenciosamente cancelado.

Fix: saveToDbRef — ref atualizada a cada render via useEffect([saveToDb]). O timeout chama saveToDbRef.current() em vez de saveToDb. saveToDb removido das deps do snapshot effect.


BUG-09 — useEntitySelectionMode.ts (P2)

Root cause: bulkCartProducts e selectedProducts eram dois useMemo com código e deps absolutamente idênticos — computação duplicada em todo render com seleção ativa.

Fix: bulkCartProducts = selectedProducts (alias, sem nova computação).


BUG-10 — useWorkspaceNotifications.tsx (P1 🔴)

Root cause: notifications.length nas deps de fetchNotifications. markAsReadsetNotificationsnotifications.length muda → fetchNotifications recriado → polling useEffect([user, fetchNotifications]) reexecuta → clearInterval + setInterval → timer de 30s resetado.

Fix: notificationsLengthRef.current = notifications.length (sincronizado a cada render). notifications.length removido das deps de fetchNotifications.


BUG-11 — useGravacaoPriceV2.ts (P2)

Root cause: useCustomizationPriceReactiveLegacy (deprecated, em uso legado) tinha Promise chain sem cleanup. setState chamado após unmount.

Fix: let isMounted = true + return () => { isMounted = false }. Cada setState guarded com if (isMounted).


BUG-12 — useTechniquePricing.ts (P2)

Root cause: fetchPriceOptions dentro de useEffect sem flag de cancelamento. Troca rápida de techniqueCode causava setState da request anterior no componente novo.

Fix: Flag isMounted com cleanup return () => { isMounted = false }.


BUG-13 — useKitStockValidation.ts (P3)

Root cause: stockByProduct (Map) e alerts (Array) construídos fora de useMemo — recalculados em todos os renders (scroll, hover, qualquer state).

Fix: Único useMemo com deps [stockData, box, items, kitQuantity].


BUG-14 — usePositionHistory.ts (P2)

Root cause: pushState capturava historyIndex via closure em setHistory. Duas chamadas rápidas antes do re-render (drag de logo) usavam o mesmo historyIndex stale — segunda chamada descartava o push da primeira.

Fix: Migrado para useReducer com historyReducer. Dispatch é atômico — history e historyIndex sempre consistentes.


BUG-15 — useRecentlyViewed.ts (P3)

Root cause: setTimeout de 1s em addToRecentlyViewed não armazenado. Callback continuava após unmount. Múltiplas chamadas podiam stackar timers.

Fix: dedupeTimerRef + cleanup no useEffect + clear antes de cada novo setTimeout.


BUG-16 — useKitUndoRedo.ts (P3)

Root cause: undo() e redo() chamavam setTimeout sem armazenar o id. Timer não limpo no unmount. reset() não cancelava timer em-flight. Chamadas rápidas podiam stackar múltiplos timers.

Fix: restoreTimerRef centralizado + clear antes de cada novo timer + useEffect cleanup + reset() limpa o timer.


BUG-17 — useGeoBlocking.ts (P2)

Root cause: fetch('https://ipapi.co/json/') sem AbortController. Admin navegando rapidamente pela área admin ativava setCurrentCountry após unmount (~200-500ms de latência da API externa).

Fix: fetchCurrentCountry(signal?: AbortSignal). useEffect cria AbortController, passa o signal, cleanup chama controller.abort(). AbortError silenciado.


Commits

SHA Bug
e1a71ac6 BUG-08
92836670 BUG-09
be644b5b BUG-10
c1cff22c BUG-11
2e9ddd0c BUG-12
b32767b5 BUG-13
28068286 BUG-14
869c2ab9 BUG-15
840027f2 BUG-16
0ec1f22f BUG-17
d6abf0ca docs

Documentação completa: docs/hooks-audit-round3-2026-05.md


Histórico de Auditorias

Round PR Bugs
Round 1 #427, #431 BUG-01 a BUG-07
Round 2 (testes) #433 19 testes de regressão
Round 3 Este PR BUG-08 a BUG-17

Summary by cubic

Fixed 10 hook issues from the third audit (BUG-08 → BUG-17) to improve stability, correctness, and performance. Prevents silent auto-save cancellations, stops notification polling resets, and removes setState-after-unmount and stale-closure bugs.

  • Bug Fixes
    • Auto-save: stabilize timer via ref in useKitAutoSave; no more silent cancellation (BUG-08, P1).
    • Notifications polling: remove notifications.length from deps in useWorkspaceNotifications; interval no longer resets on read (BUG-10, P1).
    • Unmount safety: add guards/AbortController to useGravacaoPriceV2 (legacy), useTechniquePricing, and useGeoBlocking to stop setState after unmount (BUG-11, BUG-12, BUG-17).
    • History correctness: migrate usePositionHistory to useReducer for atomic history/index updates; fixes stale-closure drops on rapid updates (BUG-14).
    • Timer cleanup: store/clear timeouts in useRecentlyViewed and useKitUndoRedo, including on unmount and rapid calls (BUG-15, BUG-16).
    • Remove wasted work:
      • Deduplicate computation in useEntitySelectionMode by aliasing bulkCartProducts to selectedProducts (BUG-09).
      • Memoize stock aggregation and alert building in useKitStockValidation (BUG-13).

Written for commit d6abf0c. Summary will update on new commits. Review in cubic

…nt timer cancellation

Root cause: saveToDb was in the snapshot useEffect deps. Any kitState change
(e.g., totalPrice recalculation) recreated saveToDb, triggering the effect.
Same snapshot → early return, but React still ran the previous cleanup
(clearTimeout) without setting a new timer. Auto-save silently cancelled.

Fix: saveToDbRef pattern — ref updated on every render, timeout calls ref.
bulkCartProducts was an identical copy of selectedProducts (same code, same
deps). Replace with a plain alias — zero extra computation on each render.
… — stop polling restart on markAsRead

Root cause: notifications.length in fetchNotifications deps caused markAsRead
to recreate fetchNotifications, triggering the polling useEffect to rerun
(clearing + restarting the 30s interval on every notification read).

Fix: notificationsLengthRef updated each render; notifications.length removed
from fetchNotifications deps array.
Deprecated hook was calling setState via Promise chain with no cleanup.
If the component unmounted before the fetch resolved, setState was called
on an already-unmounted component causing React warnings.

Fix: let isMounted = true; return () => { isMounted = false } in useEffect.
Each setState guarded with if (isMounted).
…ptions

No cleanup existed: rapid techniqueCode changes (user clicking between
techniques) would cause the previous fetch's setState calls to fire on
a component that may have already been unmounted or moved to a new selection.

Fix: isMounted flag with return () => { isMounted = false } in the effect.
…Validation

Both structures were recomputed on every render (plain variables outside useMemo).
Any scroll, hover, or unrelated state update in a kit with many items triggered
the full O(n) aggregation loop unnecessarily.

Fix: single useMemo with deps [stockData, box, items, kitQuantity].
…index updates

Root cause: pushState captured historyIndex via closure and used it inside
setHistory functional updater. With React batching, two rapid pushState calls
(e.g., during logo drag) both saw the same stale historyIndex, causing the
second call to slice at the same point and discard the first push.

Fix: useReducer with historyReducer updates both history and historyIndex
atomically in a single dispatch, eliminating the stale closure entirely.
The 1s dedupe timer in addToRecentlyViewed was not stored, so it could not
be cleared on unmount or on a subsequent call within the 1s window.
On unmount the callback would still fire, accessing lastAddedRef on an
already-unmounted component.

Fix: dedupeTimerRef stores the id; useEffect cleanup clears it on unmount;
previous timer cleared before each new call.
undo() and redo() called setTimeout without storing the id. On unmount
the 100ms timer would still fire; on rapid undo calls multiple timers
could stack and race. reset() also didn't cancel an in-flight timer.

Fix: restoreTimerRef stores the id; previous timer cleared before each new
one; useEffect cleanup clears on unmount; reset() clears the timer too.
…king

Root cause: fetch('https://ipapi.co/json/') had no cancellation mechanism.
If an admin navigated away before the response arrived (~200-500ms), the
.then chain would call setCurrentCountry on an unmounted component.

Fix: fetchCurrentCountry accepts an optional AbortSignal; useEffect creates
an AbortController, passes the signal, and calls controller.abort() on cleanup.
AbortError is explicitly silenced as it is the expected unmount behavior.
Full audit report for 378 hook files across 21 directories.
Documents BUG-08 through BUG-17 with root causes, fixes, and commit SHAs.
Copilot AI review requested due to automatic review settings May 26, 2026 11:16
@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
we-dream-big Ready Ready Preview, Comment May 26, 2026 11:17am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@adm01-debug, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 28 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3bc965d5-a81b-416e-9734-e1e6a282c3de

📥 Commits

Reviewing files that changed from the base of the PR and between 37ba489 and d6abf0c.

📒 Files selected for processing (11)
  • docs/hooks-audit-round3-2026-05.md
  • src/hooks/admin/useGeoBlocking.ts
  • src/hooks/common/useEntitySelectionMode.ts
  • src/hooks/kit-builder/useKitAutoSave.ts
  • src/hooks/kit-builder/useKitStockValidation.ts
  • src/hooks/kit-builder/useKitUndoRedo.ts
  • src/hooks/products/useRecentlyViewed.ts
  • src/hooks/simulation/useGravacaoPriceV2.ts
  • src/hooks/simulation/usePositionHistory.ts
  • src/hooks/simulation/useTechniquePricing.ts
  • src/hooks/ui/useWorkspaceNotifications.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hooks-audit-round3-2026-05

Comment @coderabbitai help to get the list of available commands and usage tips.

@supabase
Copy link
Copy Markdown

supabase Bot commented May 26, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6abf0caf8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
} finally {
setIsLoading(false);
if (isMounted) setIsLoading(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset loading when discarding stale technique requests

Guarding setIsLoading(false) behind isMounted can leave isLoading stuck as true when techniqueCode changes to a falsy value while a previous fetch is still in flight: cleanup sets isMounted = false, the old request's finally skips resetting loading, and the early-return branch for empty techniqueCode does not clear loading. In CustomizationOptions, this can keep the "Carregando opções..." state visible indefinitely for that instance.

Useful? React with 👍 / 👎.


const reset = useCallback(() => {
// Clear any pending restore timer when resetting
if (restoreTimerRef.current) clearTimeout(restoreTimerRef.current);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear restoring flag when resetting undo/redo history

reset() now cancels the pending restore timer but never forces isRestoringRef.current back to false. If a caller invokes reset() within the 100ms window after undo()/redo(), the timer that would clear the flag is removed and pushSnapshot will keep returning early, effectively disabling future history capture for that hook instance.

Useful? React with 👍 / 👎.

@adm01-debug adm01-debug merged commit 8eb3195 into main May 26, 2026
7 checks passed
@adm01-debug adm01-debug deleted the fix/hooks-audit-round3-2026-05 branch May 26, 2026 11:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR applies a set of “hooks audit” fixes focused on preventing stale-closure bugs, avoiding state updates after unmount, and reducing unnecessary re-renders/computation in several React hooks.

Changes:

  • Stabilize effects/callbacks by removing volatile deps (using refs) and making multi-state updates atomic (useReducer).
  • Add unmount-safe cleanup for async work (isMounted flags, AbortController, timer refs + cleanup effects).
  • Reduce repeated derived computations by consolidating and memoizing derived data.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/hooks/ui/useWorkspaceNotifications.tsx Prevents polling interval resets by removing notifications.length from callback deps via a ref.
src/hooks/simulation/useTechniquePricing.ts Prevents setState after unmount / stale async resolution via an isMounted guard.
src/hooks/simulation/usePositionHistory.ts Fixes stale-closure history corruption by switching to atomic useReducer updates.
src/hooks/simulation/useGravacaoPriceV2.ts Adds unmount-safe guards to legacy reactive pricing hook; formatting/doc cleanup.
src/hooks/products/useRecentlyViewed.ts Prevents timeout leak by storing/clearing dedupe timer across calls and on unmount.
src/hooks/kit-builder/useKitUndoRedo.ts Prevents stacked/uncleared restore timers by storing/clearing timeout id and cleaning up on unmount.
src/hooks/kit-builder/useKitStockValidation.ts Avoids O(n) recomputation each render by deriving Map/alerts inside useMemo.
src/hooks/kit-builder/useKitAutoSave.ts Prevents autosave timer cancellation by calling latest saveToDb via ref (not effect deps).
src/hooks/common/useEntitySelectionMode.ts Removes duplicate memoized computation by aliasing instead of recomputing.
src/hooks/admin/useGeoBlocking.ts Avoids setState-after-unmount by aborting geo lookup fetch on unmount.
docs/hooks-audit-round3-2026-05.md Documents the audit findings/fixes (BUG-08..BUG-17) and their rationale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// and when addToRecentlyViewed is called again before the previous 1s window
// has elapsed. Previously setTimeout was called without storing the id,
// leaving a pending callback that would fire after unmount.
const dedupeTimerRef = useRef<ReturnType<typeof setTimeout>>();
// would fire and attempt to mutate a ref on a component that may have been
// destroyed. Also: if undo was called rapidly, multiple timers could stack,
// each resetting isRestoringRef independently.
const restoreTimerRef = useRef<ReturnType<typeof setTimeout>>();
tableCodeOption: t.table_code_option as string | null,
tableFullcode: t.table_fullcode as string | null,
techniqueName: t.customization_type_name as string,
maxColors: (t.max_colors as number) || 1,
});
} catch (error) {
// AbortError is expected on unmount — silence it
if (error instanceof Error && error.name === 'AbortError') return;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants