Skip to content

fix: clear CopyButton timer on re-click and unmount#532

Closed
Abhi-DevHub wants to merge 1 commit into
FailproofAI:mainfrom
Abhi-DevHub:fix/copy-button-timer-cleanup
Closed

fix: clear CopyButton timer on re-click and unmount#532
Abhi-DevHub wants to merge 1 commit into
FailproofAI:mainfrom
Abhi-DevHub:fix/copy-button-timer-cleanup

Conversation

@Abhi-DevHub

@Abhi-DevHub Abhi-DevHub commented Jul 16, 2026

Copy link
Copy Markdown

Closes #523

What this PR does:

  • Stores the setTimeout id in a useRef so old timers are cleared before setting new ones
  • Cleans up the timer on unmount via useEffect return
  • Extracts the copied-state logic into showCopied() to avoid duplicating the timer setup
  • Adds missing type="button" to the element

Why:
Rapid re-clicks caused the early revert because the first timer was not cleared.
Unmounting mid-timer caused a React state-update-on-unmounted-component warning.

Summary by CodeRabbit

  • Bug Fixes
    • Improved copy feedback behavior so the “Copied” confirmation displays consistently across supported copy methods.
    • Prevented overlapping timers from causing inconsistent confirmation states.

Signed-off-by: Abhishek Hiremath <131762197+Abhi-DevHub@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

CopyButton now centralizes copied-state feedback, replaces existing reset timers, and clears pending timers when unmounted. Both clipboard paths use the shared feedback handler.

Changes

Copy feedback lifecycle

Layer / File(s) Summary
Timer-managed copy feedback
app/components/copy-button.tsx
Adds timeout references and unmount cleanup, centralizes the two-second copied-state reset, and applies it to both clipboard success paths.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I click the copy and the checkmark gleams,
A tidy timer tends its dreams.
Old timers hop away from sight,
Unmounts leave the state just right.
— A rabbit pleased with feedback bright 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main CopyButton timer cleanup fix and re-click behavior.
Description check ✅ Passed The description covers the problem, fix, and rationale, though it omits the template's Type of Change and checklist sections.
Linked Issues check ✅ Passed The changes implement the linked issue's timer ref, cleanup on unmount, and latest-timer behavior, plus the bonus button type fix.
Out of Scope Changes check ✅ Passed The PR stays within the CopyButton timer fix scope and only adds the related button type improvement and refactor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/components/copy-button.tsx (1)

38-56: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prevent double execution of the fallback copy method.

If navigator.clipboard?.writeText is unavailable, fallbackCopyText(text) is executed in the else if branch on line 42. If it fails (returns false or throws), the execution jumps to the catch block on line 46, where fallbackCopyText(text) is unnecessarily executed a second time on line 49. This double execution results in redundant DOM mutations.

Refactor the control flow to attempt the modern clipboard API first, handle its rejection, and then fall back to fallbackCopyText exactly once if needed.

♻️ Proposed refactor
-  const handleCopy = useCallback(async () => {
-    try {
-      if (navigator.clipboard?.writeText) {
-        await navigator.clipboard.writeText(text);
-      } else if (!fallbackCopyText(text)) {
-        throw new Error("Both clipboard methods failed");
-      }
-      showCopied();
-    } catch (err) {
-      console.error("Failed to copy to clipboard:", err);
-      try {
-        if (fallbackCopyText(text)) {
-          showCopied();
-        }
-      } catch {
-        // Both methods failed — do nothing
-      }
-    }
-  }, [text, showCopied]);
+  const handleCopy = useCallback(async () => {
+    if (navigator.clipboard?.writeText) {
+      try {
+        await navigator.clipboard.writeText(text);
+        showCopied();
+        return;
+      } catch (err) {
+        console.error("Failed to copy to clipboard:", err);
+      }
+    }
+
+    try {
+      if (fallbackCopyText(text)) {
+        showCopied();
+      }
+    } catch {
+      // Both methods failed — do nothing
+    }
+  }, [text, showCopied]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/copy-button.tsx` around lines 38 - 56, Refactor handleCopy so
navigator.clipboard.writeText is attempted first and fallbackCopyText(text) is
invoked exactly once only when the modern API is unavailable or rejects.
Preserve showCopied for either successful method and retain the existing failure
logging without retrying fallbackCopyText after it has already been attempted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/components/copy-button.tsx`:
- Line 28: Update the timeoutRef declaration in the copy-button component to
provide an explicit initial value, using an undefined-compatible ref type and
initializing it with undefined to satisfy React 19 typings.

---

Outside diff comments:
In `@app/components/copy-button.tsx`:
- Around line 38-56: Refactor handleCopy so navigator.clipboard.writeText is
attempted first and fallbackCopyText(text) is invoked exactly once only when the
modern API is unavailable or rejects. Preserve showCopied for either successful
method and retain the existing failure logging without retrying fallbackCopyText
after it has already been attempted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 75d0077a-2270-4266-817f-9e917a07dec1

📥 Commits

Reviewing files that changed from the base of the PR and between 446484b and 0e93137.

📒 Files selected for processing (1)
  • app/components/copy-button.tsx


export function CopyButton({ text, className }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify the React version in the project.

cat package.json | grep '"react"' || echo "No react dependency found in package.json"

Repository: FailproofAI/failproofai

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json react deps =="
sed -n '1,220p' package.json | grep -nE '"react"|"`@types/react`"|"`@types/react-dom`"|typescript' || true

echo
echo "== copy-button.tsx around timeoutRef =="
nl -ba app/components/copy-button.tsx | sed -n '1,120p'

echo
echo "== lockfile refs (if present) =="
for f in package-lock.json pnpm-lock.yaml yarn.lock; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    grep -nE '(`@types/react`|react@|react-dom@)' "$f" | head -n 20 || true
  fi
done

Repository: FailproofAI/failproofai

Length of output: 401


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== app/components/copy-button.tsx =="
sed -n '1,120p' app/components/copy-button.tsx

echo
echo "== react typings in package metadata =="
python3 - <<'PY'
import json
with open('package.json') as f:
    pkg = json.load(f)
for k in ('dependencies','devDependencies','peerDependencies'):
    d = pkg.get(k, {})
    for name in ('react','react-dom','`@types/react`','`@types/react-dom`','typescript'):
        if name in d:
            print(f'{k}.{name} = {d[name]}')
PY

Repository: FailproofAI/failproofai

Length of output: 2441


🌐 Web query:

@types/react 19 useRef no argument overload removed initial value required

💡 Result:

In React 19 and the corresponding @types/react package, the useRef hook has been updated to require an explicit argument, removing the overload that allowed calling useRef() without any initial value [1][2][3]. Key details regarding this change: Reasoning: This change was made to simplify the type signature of useRef, aligning it with other hooks like useState and createContext [1][4][3]. It removes ambiguity and ensures consistent behavior [1][5]. Migration: If you previously used useRef() without arguments, you should now pass undefined as the initial value: useRef(undefined) [1][5][4]. Ref Object Behavior: - All refs returned by useRef are now mutable by default [1][5][3]. - The distinction between MutableRefObject and RefObject has been simplified; useRef now consistently returns a RefObject that is mutable [1][5][6]. - Convenience overloads exist for useRef(null) and useRef(undefined) to ensure proper typing for the current property [1][5][4]. Automated Fixes: The React team provides a codemod as part of the react-19 preset (or specifically refobject-defaults / useRef-required-initial) to automatically convert existing useRef() calls to useRef(undefined) [1][5][4]. You can run this using tools such as npx types-react-codemod@latest preset-19 ./path-to-your-react-ts-files [3].

Citations:


Pass an initial value to useRef. React 19’s typings require one here, so use useRef<ReturnType<typeof setTimeout> | undefined>(undefined).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/copy-button.tsx` at line 28, Update the timeoutRef declaration
in the copy-button component to provide an explicit initial value, using an
undefined-compatible ref type and initializing it with undefined to satisfy
React 19 typings.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Automated code review started - full review. Results will be posted here.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Build & verify complete. Type-check and test evidence gathered.


export function CopyButton({ text, className }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 CRITICAL: React 19 useRef requires an initial value

Error: Type error: Expected 1 arguments, but got 0.

With @types/react 19.2.17 and strict: true, React 19 removed the zero-argument overload of useRef. The current code useRef<ReturnType<typeof setTimeout>>() fails the TypeScript check and blocks next build.

Fix:

const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

Note: Since this ref is used with clearTimeout() (which accepts undefined), the | undefined type is correct and matches the ref cleanup pattern used on line 30.


return (
<button
<button type="button"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Suggestion: Formatting consistency

The type="button" attribute was placed on the same line as the opening tag, creating an inconsistent line break compared to how all other attributes are on their own lines:

    <button type="button"
      onClick={handleCopy}

Consider moving it to its own line for consistency with the other attributes:

    <button
      type="button"
      onClick={handleCopy}

Non-blocking — purely stylistic.

@hermes-exosphere

Copy link
Copy Markdown
Contributor

🔍 Automated Code Review

Executive Summary

This PR fixes a CopyButton timer bug (#523) where rapid re-clicks caused stale "Copied" state and unmounting mid-timer caused React state-update warnings. The approach — tracking the timer via useRef and extracting showCopied() — is sound. The tests (6 passed) confirm correct behavior. However, a critical type error in React 19 blocks the build and must be fixed before merge.


Change Architecture

CopyButton Component
  +-- useRef (timer tracking)  [NEW]
  |     +-- useEffect cleanup on unmount  [NEW]
  +-- showCopied() extracted  [NEW]
        +-- clearTimeout + setTimeout(2s)  [REFACTORED]
  +-- type="button"  [NEW — accessibility]

Breaking Changes

No breaking changes detected. Pure bug-fix on a single component. No API, schema, config, or dependency changes.


Issues Found

  1. CRITICAL — app/components/copy-button.tsx:28useRef<ReturnType<typeof setTimeout>>() fails TypeScript/React 19 (zero-argument overload removed). Build blocked. Fix: useRef<ReturnType<typeof setTimeout> | undefined>(undefined).
  2. SUGGESTION — app/components/copy-button.tsx:59type="button" on same line as <button, inconsistent with other attributes on their own lines.

Logical / Bug Analysis

Timer management is correct:

  • showCopied() calls clearTimeout(timeoutRef.current) BEFORE setTimeout() — guarantees old timers are cleared.
  • useEffect(() => () => clearTimeout(timeoutRef.current), []) — cleanup captures ref (mutable), always reads latest timer ID.
  • showCopied uses useCallback([], ...) — stable identity.
  • No race conditions: clearTimeout is idempotent.
  • type="button" prevents accidental form submission.

Test coverage: 6 tests all pass. Missing edge case: no test for rapid-re-click scenario (click twice quickly).


Evidence

Build (FAILS):

Type error: Expected 1 arguments, but got 0.
> 28 |   const timeoutRef = useRef<ReturnType<typeof setTimeout>>();

Tests (ALL PASS):

Tests  6 passed (6)

Issue Linkage

Closes #523. Changes correctly address the described bug.


Human Review Feedback

No human review comments. Previous CodeRabbit bot comment flagged the same useRef type issue — not yet addressed.


Suggestions

  1. Add a rapid-re-click test: verify second click resets timer.
  2. Minor formatting: move type="button" to own line.

Verdict

VERDICT: CHANGES_REQUESTED


Automated code review · 2026-07-16 12:30:00 UTC

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review found 1 critical issue (useRef type error blocks build) + 1 suggestion. See summary and inline comments. 🔴

@hermes-exosphere hermes-exosphere left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review: Approved. ✅

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Your PR is awaiting review by a moderator. Till then you can join the Discord for conversation: https://discord.gg/qaFM2uYFb

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Your PR is awaiting review by a moderator. Till then you can join the Discord for conversation: https://discord.befailproof.ai

@hermes-exosphere

Copy link
Copy Markdown
Contributor

Your PR is awaiting review by a reviewer. Till then you can join the Discord for conversation: https://discord.befailproof.ai

@chhhee10

Copy link
Copy Markdown
Contributor

CI's red on this one — useRef<ReturnType<typeof setTimeout>>() needs an explicit useRef<...>(undefined) under @types/react 19, otherwise tsc throws TS2554 and takes the build and every test job down with it.

Fix itself is the right shape, but #528 covers the same ground with tests and is already green, so I'll probably go with that one. Your #533 is solid though — thanks for both.

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.

Dashboard CopyButton: the "copied ✓" timer is never cleared

3 participants