Click Path Audit
Community skill. Full credit goes to the original author.
Find bugs that code checks may miss. These bugs happen when state changes clash, run in the wrong order, or undo each other.
Use This Skill When
Use this skill when:
- A button looks broken, but its handler does run.
- A form ends in the wrong state.
- A menu, modal, or panel opens and then closes.
- Two state actions work alone but fail when used together.
- A slow request can replace newer data.
- A large code change touched shared state.
- Tests pass, but the UI still feels wrong.
What This Skill Checks
Normal checks often ask:
- Does the function exist?
- Is the event linked to it?
- Does the code crash?
- Does the function return the right type?
This audit also asks:
- Does the final UI match the button label?
- Does a later call undo an earlier call?
- Does a shared state action clear state it does not own?
- Can two async tasks finish in the wrong order?
- Can a second click run before the first click ends?
- Can a page change or unmount leave stale work running?
- Do error and cancel paths restore the right state?
- Does saved or local state bring back an old value?
- Does a form submit twice from both click and submit events?
Core Rule
Trace the full path in the exact order it can run.
Do not stop at the click handler. Follow every call that can read or write state. Include state actions, hooks, effects, request callbacks, timers, route changes, and cleanup code.
Step 1: Map Shared State
Before checking buttons, list each state action in the target area.
For each action, record:
- State it reads.
- State it sets.
- State it clears.
- Other actions it calls.
- Async work it starts.
- Effects that run after its state change.
Use this form:
STORE: emailStore
ACTION: setComposeMode(value)
reads: none
sets: composeMode
clears: none
ACTION: selectThread(thread)
reads: thread
sets: selectedThread, messages, drafts
clears: composeData, selectedDraft
also sets: composeMode = false
RISKY RESET:
selectThread changes composeMode.
composeMode is mainly owned by setComposeMode.Mark an action as risky when it:
- Clears many fields.
- Changes state owned by another feature.
- Uses old state after async work.
- Has a broad name such as
reset,clearAll, orselect. - Starts work that has no cancel or stale-result check.
Step 2: List Every Touch Point
Find all user actions in the target area.
Include:
- Buttons and links.
- Form submit events.
- Inputs, selects, and toggles.
- Menu items.
- Keyboard shortcuts.
- Drag and drop.
- Swipe and touch events.
- Route changes.
- Auto-save and timer actions.
For each one, record:
TOUCH POINT: New Email
LOCATION: ComposeButton.tsx:24
EVENT: onClick
EXPECTED END STATE: Compose view is open with no thread selected.If the label is not clear, use the nearby text and UI flow to state the likely goal. Mark it as an assumption.
Step 3: Trace Calls in Order
Write every call in the order it starts.
For each call, note:
- What it reads.
- What it writes.
- What it clears.
- Whether it is sync or async.
- What runs after it ends.
- What can run at the same time.
Use this form:
CALL PATH:
1. setComposeMode(true)
writes: composeMode = true
2. selectThread(null)
writes: selectedThread = null
also writes: composeMode = false
FINAL STATE:
composeMode = false
selectedThread = null
EXPECTED STATE:
composeMode = true
selectedThread = null
RESULT:
Bug found. Step 2 undoes step 1.Step 4: Check Every End Path
Check more than the success path.
Check:
- Success.
- Error.
- Cancel.
- Timeout.
- Double click.
- Fast repeat click.
- Back button.
- Route change.
- Component unmount.
- Empty data.
- Missing data.
- Old saved state.
- Slow first request and fast second request.
Ask these questions:
- What is the final state?
- Does it match what the user asked for?
- Did a later call undo an earlier call?
- Can old async work replace new state?
- Can an effect run and change the state again?
- Is loading cleared on every path?
- Is an error shown and then cleared by mistake?
- Can the same action run twice?
- Can cleanup code clear state used by a new screen?
- Can local storage or cached data restore a bad value?
Step 5: Report Findings
Report each bug with proof. Do not say only that a call is unsafe.
Use this form:
FINDING: New Email closes itself
SEVERITY: High
LOCATION: ComposeButton.tsx:24
EXPECTED: Open a blank compose view.
ACTUAL: Compose mode ends as false.
CALL ORDER:
1. setComposeMode(true)
2. selectThread(null)
3. selectThread sets composeMode to false
CAUSE:
selectThread clears state owned by the compose flow.
FIX IDEA:
Select or clear the thread before opening compose mode.
Better: split thread selection from compose cleanup.
TEST:
Click New Email while a thread is open.
Check that composeMode is true and selectedThread is null.Use these levels:
- High: The main action fails or data may be lost.
- Medium: The UI ends in the wrong state but the user can recover.
- Low: The state is odd but has little user harm.
Concrete Example
A search box sends a request after each change:
async function onSearch(text) {
setQuery(text);
const results = await search(text);
setResults(results);
}The user types cat, then quickly types cats.
Possible order:
1. Start request for "cat".
2. Start request for "cats".
3. "cats" finishes. The UI shows results for "cats".
4. "cat" finishes. The UI now shows old results for "cat".Audit result:
FINDING: Old search results replace new results
SEVERITY: Medium
EXPECTED: Results match "cats".
ACTUAL: Results may match "cat".
CAUSE: The first request can finish last.
FIX IDEA: Cancel the old request or check that its query is still current.
TEST: Delay the "cat" request and let the "cats" request finish first.Completion Check
The audit is done when:
- Every touch point in scope is listed.
- Every call path is traced in order.
- Shared state writes and clears are mapped.
- Success, error, cancel, and async paths are checked.
- Each finding shows the expected and real final state.
- Each finding has a clear test.
- Areas not checked are listed as out of scope.