Skip to content

fix: add debounced prop and ux to mui search input component#209

Open
tomrndom wants to merge 5 commits intomainfrom
fix/mui-search-input-debounced
Open

fix: add debounced prop and ux to mui search input component#209
tomrndom wants to merge 5 commits intomainfrom
fix/mui-search-input-debounced

Conversation

@tomrndom
Copy link
Copy Markdown
Contributor

@tomrndom tomrndom commented Apr 2, 2026

ref: https://app.clickup.com/t/86b84rhkb

Signed-off-by: Tomás Castillo tcastilloboireau@gmail.com

Summary by CodeRabbit

  • New Features

    • Optional debounced search execution to reduce search frequency
    • Search icon added to the start of the input for clearer affordance
  • Bug Fixes / Improvements

    • Enter key now triggers search only when debounced is not enabled
    • Pending debounced searches are canceled when cleared or when the input unmounts
  • Tests

    • Added tests validating debounced typing behavior and Enter-key interactions

Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

Warning

Rate limit exceeded

@tomrndom has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 0 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 10 minutes and 0 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8d4e7b0c-0e4b-4182-99a8-c369a9a7429c

📥 Commits

Reviewing files that changed from the base of the PR and between 0dd6a00 and 9f8f4b0.

📒 Files selected for processing (1)
  • src/components/mui/search-input.js
📝 Walkthrough

Walkthrough

SearchInput adds an optional debounced prop that wraps onSearch with a lodash debounce (canceled on unmount). Input changes update local state; Enter triggers immediate search only when debounced is falsy. A search icon was added as a startAdornment.

Changes

Cohort / File(s) Summary
SearchInput component
src/components/mui/search-input.js
Adds debounced prop and memoized lodash debounce using DEBOUNCE_WAIT; invokes debounced onSearch from onChange when enabled, cancels pending calls on clear and unmount. Refactors onChange/onKeyDown so Enter triggers immediate search only when not debounced. Adds startAdornment search icon; preserves clear/end-adornment behavior.
Tests for SearchInput
src/components/mui/__tests__/search-input.test.js
Updates imports to include act and DEBOUNCE_WAIT. Adds tests verifying that with debounced: true typing does not call onSearch immediately but does after advancing timers by DEBOUNCE_WAIT, and that Enter does not trigger search when debounced. Also asserts default (non-debounced) typing does not call onSearch without Enter.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I tap the keys with patient paws,
Timers hum and silence flaws.
Enter waits while dots align,
A tiny search icon gives a sign.
Hop—debounced results arrive just fine. 🥕🔍

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding a debounced prop and associated UX behavior to the MUI search input component.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mui-search-input-debounced

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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)
src/components/mui/search-input.js (1)

58-75: ⚠️ Potential issue | 🟡 Minor

Two issues with adornment logic.

  1. Duplicate search icons: With the new startAdornment always showing a SearchIcon (lines 58-62), the fallback SearchIcon in endAdornment (lines 72-74) creates duplicate icons when no term exists. The end SearchIcon should be removed.

  2. Clear button uses prop instead of state: The condition term ? ... references the prop, not the local searchTerm state. If the user types something but the parent hasn't updated the term prop yet, the clear button won't appear. Use searchTerm for immediate feedback.

🐛 Proposed fix
           startAdornment: (
             <InputAdornment position="start">
               <SearchIcon sx={{ color: "#0000008F" }} />
             </InputAdornment>
           ),
-          endAdornment: term ? (
+          endAdornment: searchTerm ? (
             <IconButton
               size="small"
               onClick={handleClear}
               sx={{ position: "absolute", right: 0 }}
             >
               <ClearIcon sx={{ color: "#0000008F" }} />
             </IconButton>
-          ) : (
-            <SearchIcon
-              sx={{ mr: 1, color: "#0000008F", position: "absolute", right: 0 }}
-            />
-          )
+          ) : null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/search-input.js` around lines 58 - 75, The adornment logic
currently renders a SearchIcon in both startAdornment and in the endAdornment
fallback and uses the prop term to decide showing the clear button; to fix this
remove the fallback SearchIcon from endAdornment so only the startAdornment
SearchIcon is rendered, and change the clear-button condition to use the local
searchTerm state (instead of the prop term) so the IconButton with
onClick={handleClear} appears immediately when the user types; update the JSX
that defines startAdornment and endAdornment (InputAdornment, SearchIcon,
IconButton, ClearIcon, handleClear, term -> searchTerm) accordingly.
🧹 Nitpick comments (1)
src/components/mui/search-input.js (1)

45-49: Consider allowing Enter key to trigger immediate search even in debounced mode.

Currently, pressing Enter does nothing when debounced is true. Users commonly expect Enter to immediately submit their search query. Consider canceling the pending debounce and triggering the search immediately on Enter.

💡 Optional enhancement
   const handleKeyDown = (ev) => {
-    if (!debounced && ev.key === "Enter") {
+    if (ev.key === "Enter") {
+      onSearchDebounced?.cancel();
       onSearch(searchTerm);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/search-input.js` around lines 45 - 49, handle the Enter
key in handleKeyDown so it triggers an immediate search even when debounced is
true: detect ev.key === "Enter", clear/cancel any pending debounce timer or
cancel the debounced call (e.g., clearTimeout on a timeoutId or call
debounced.cancel if using lodash/debounce), then call onSearch(searchTerm)
immediately; reference handleKeyDown, debounced, onSearch, and searchTerm so the
fix cancels the pending debounce and runs the search right away.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/mui/search-input.js`:
- Around line 33-36: Replace useCallback with useMemo when creating the
debounced function: the current onSearchDebounced is memoizing the result of
debounce((value) => onSearch(value), DEBOUNCE_WAIT) so wrap that expression in
useMemo and include onSearch and debounced in the dependency array (and
debounce/DEBOUNCE_WAIT if not stable) so a stable debounced function instance is
returned; ensure the import for useMemo is added and that when debounced is
false you return null (same behavior) to preserve existing logic.

---

Outside diff comments:
In `@src/components/mui/search-input.js`:
- Around line 58-75: The adornment logic currently renders a SearchIcon in both
startAdornment and in the endAdornment fallback and uses the prop term to decide
showing the clear button; to fix this remove the fallback SearchIcon from
endAdornment so only the startAdornment SearchIcon is rendered, and change the
clear-button condition to use the local searchTerm state (instead of the prop
term) so the IconButton with onClick={handleClear} appears immediately when the
user types; update the JSX that defines startAdornment and endAdornment
(InputAdornment, SearchIcon, IconButton, ClearIcon, handleClear, term ->
searchTerm) accordingly.

---

Nitpick comments:
In `@src/components/mui/search-input.js`:
- Around line 45-49: handle the Enter key in handleKeyDown so it triggers an
immediate search even when debounced is true: detect ev.key === "Enter",
clear/cancel any pending debounce timer or cancel the debounced call (e.g.,
clearTimeout on a timeoutId or call debounced.cancel if using lodash/debounce),
then call onSearch(searchTerm) immediately; reference handleKeyDown, debounced,
onSearch, and searchTerm so the fix cancels the pending debounce and runs the
search right away.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36d2f051-31fb-4dac-9a9d-5cab289c16a9

📥 Commits

Reviewing files that changed from the base of the PR and between 54bca4e and 10075aa.

📒 Files selected for processing (1)
  • src/components/mui/search-input.js

tomrndom added 2 commits April 2, 2026 15:29
Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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)
src/components/mui/search-input.js (1)

58-75: ⚠️ Potential issue | 🟠 Major

Use searchTerm for end-adornment state and remove the duplicate search icon.

Line 63 currently keys off term, which can be stale in the new non-debounced flow (typing updates local state but not parent state yet). Also, with the new start adornment (Lines 58-62), the fallback right-side search icon (Lines 71-74) creates duplicate icons.

♻️ Proposed fix
-          endAdornment: term ? (
+          endAdornment: searchTerm ? (
             <IconButton
               size="small"
               onClick={handleClear}
               sx={{ position: "absolute", right: 0 }}
             >
               <ClearIcon sx={{ color: "#0000008F" }} />
             </IconButton>
-          ) : (
-            <SearchIcon
-              sx={{ mr: 1, color: "#0000008F", position: "absolute", right: 0 }}
-            />
-          )
+          ) : null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/search-input.js` around lines 58 - 75, The endAdornment
currently checks the stale prop/local variable term and renders a duplicate
SearchIcon on the right; change the condition to use the controlled value
searchTerm (the up-to-date state/prop) when deciding to show the Clear button,
and remove the right-side fallback SearchIcon since you already render a
startAdornment SearchIcon; update the endAdornment logic around IconButton and
handleClear to rely on searchTerm and render null (or nothing) when searchTerm
is empty so no duplicate icon appears.
🧹 Nitpick comments (1)
src/components/mui/__tests__/search-input.test.js (1)

63-96: Add a regression test for clearing with a pending debounced call.

Given the new debounced flow, we should explicitly assert that clear does not allow a previously typed value to fire later.

➕ Suggested test case
+    test("clearing input cancels pending debounced search", async () => {
+      const onSearch = jest.fn();
+      const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
+      render(<SearchInput term="initial" onSearch={onSearch} debounced />);
+
+      const input = screen.getByPlaceholderText("Search...");
+      await user.clear(input);
+      await user.type(input, "something");
+      await user.click(screen.getByRole("button"));
+
+      act(() => jest.advanceTimersByTime(DEBOUNCE_WAIT));
+      expect(onSearch).toHaveBeenCalledWith("");
+      expect(onSearch).not.toHaveBeenCalledWith("something");
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/__tests__/search-input.test.js` around lines 63 - 96, Add
a regression test in the "debounced prop" suite for SearchInput that ensures
clearing cancels a pending debounced call: render SearchInput with debounced and
a jest.fn() onSearch, use userEvent.setup({ advanceTimers:
jest.advanceTimersByTime }) to type a value, then trigger the component's clear
action (e.g., click the clear button or invoke the clear control exposed by
SearchInput), advance timers with act(() =>
jest.advanceTimersByTime(DEBOUNCE_WAIT)) and assert onSearch was not called;
keep the test alongside the existing tests and reuse DEBOUNCE_WAIT, SearchInput,
and onSearch identifiers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/mui/search-input.js`:
- Around line 40-43: handleClear can allow a previously scheduled trailing call
from handleChange to fire after the input is cleared; fix by cancelling any
pending debounced work before clearing. In the component, call the debounced
function's cancel method (e.g., debounced.cancel()) if present at the start of
handleClear, then proceed to setSearchTerm('') and emit any immediate clear
behavior (e.g., call onSearch or onSearchDebounced with an empty string as
needed), guarding for the existence of debounced and onSearchDebounced.

---

Outside diff comments:
In `@src/components/mui/search-input.js`:
- Around line 58-75: The endAdornment currently checks the stale prop/local
variable term and renders a duplicate SearchIcon on the right; change the
condition to use the controlled value searchTerm (the up-to-date state/prop)
when deciding to show the Clear button, and remove the right-side fallback
SearchIcon since you already render a startAdornment SearchIcon; update the
endAdornment logic around IconButton and handleClear to rely on searchTerm and
render null (or nothing) when searchTerm is empty so no duplicate icon appears.

---

Nitpick comments:
In `@src/components/mui/__tests__/search-input.test.js`:
- Around line 63-96: Add a regression test in the "debounced prop" suite for
SearchInput that ensures clearing cancels a pending debounced call: render
SearchInput with debounced and a jest.fn() onSearch, use userEvent.setup({
advanceTimers: jest.advanceTimersByTime }) to type a value, then trigger the
component's clear action (e.g., click the clear button or invoke the clear
control exposed by SearchInput), advance timers with act(() =>
jest.advanceTimersByTime(DEBOUNCE_WAIT)) and assert onSearch was not called;
keep the test alongside the existing tests and reuse DEBOUNCE_WAIT, SearchInput,
and onSearch identifiers.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0184f1f4-9eaf-4b46-b783-a52629fbeb79

📥 Commits

Reviewing files that changed from the base of the PR and between baef896 and 39bac29.

📒 Files selected for processing (2)
  • src/components/mui/__tests__/search-input.test.js
  • src/components/mui/search-input.js

Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
src/components/mui/search-input.js (1)

59-76: ⚠️ Potential issue | 🟠 Major

Duplicate search icons when term is falsy.

The startAdornment always renders a SearchIcon (lines 59-63), and when term is falsy, the endAdornment also renders a SearchIcon (lines 73-75). This results in two search icons displayed simultaneously when the input is empty.

🐛 Proposed fix: Remove the redundant endAdornment SearchIcon
           endAdornment: term ? (
             <IconButton
               size="small"
               onClick={handleClear}
               sx={{ position: "absolute", right: 0 }}
             >
               <ClearIcon sx={{ color: "#0000008F" }} />
             </IconButton>
-          ) : (
-            <SearchIcon
-              sx={{ mr: 1, color: "#0000008F", position: "absolute", right: 0 }}
-            />
-          )
+          ) : null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/search-input.js` around lines 59 - 76, The UI renders
duplicate SearchIcon components because startAdornment always renders a
SearchIcon and endAdornment renders another SearchIcon when term is falsy;
update the endAdornment logic in the component (the JSX that sets startAdornment
and endAdornment) to remove the redundant SearchIcon branch so endAdornment only
renders the IconButton with ClearIcon when term is truthy (using term and
handleClear) and otherwise renders nothing; keep the startAdornment
InputAdornment with SearchIcon intact and ensure IconButton/clear flow remains
tied to handleClear and ClearIcon.
🧹 Nitpick comments (1)
src/components/mui/search-input.js (1)

28-37: Consider reordering declarations for clarity.

handleClear references onSearchDebounced before it's declared. This works at runtime (the closure is only evaluated when the click handler fires), but it harms readability and can confuse static analysis tools or future maintainers.

♻️ Suggested reordering
+  const onSearchDebounced = useMemo(
+    () => debounced ? debounce((value) => onSearch(value), DEBOUNCE_WAIT) : null,
+    [onSearch, debounced]
+  );
+
   const handleClear = () => {
     onSearchDebounced?.cancel();
     setSearchTerm("");
     onSearch("");
   };
-
-  const onSearchDebounced = useMemo(
-    () => debounced ? debounce((value) => onSearch(value), DEBOUNCE_WAIT) : null,
-    [onSearch, debounced]
-  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/search-input.js` around lines 28 - 37, Move the
onSearchDebounced declaration (the useMemo that returns debounce((value) =>
onSearch(value), DEBOUNCE_WAIT) or null) above the handleClear function so
handleClear no longer references onSearchDebounced before it's declared; update
handleClear to keep its current logic (onSearchDebounced?.cancel(),
setSearchTerm(""), onSearch("")) but ensure onSearchDebounced is defined earlier
and that the dependency array on the useMemo remains [onSearch, debounced].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/components/mui/search-input.js`:
- Around line 59-76: The UI renders duplicate SearchIcon components because
startAdornment always renders a SearchIcon and endAdornment renders another
SearchIcon when term is falsy; update the endAdornment logic in the component
(the JSX that sets startAdornment and endAdornment) to remove the redundant
SearchIcon branch so endAdornment only renders the IconButton with ClearIcon
when term is truthy (using term and handleClear) and otherwise renders nothing;
keep the startAdornment InputAdornment with SearchIcon intact and ensure
IconButton/clear flow remains tied to handleClear and ClearIcon.

---

Nitpick comments:
In `@src/components/mui/search-input.js`:
- Around line 28-37: Move the onSearchDebounced declaration (the useMemo that
returns debounce((value) => onSearch(value), DEBOUNCE_WAIT) or null) above the
handleClear function so handleClear no longer references onSearchDebounced
before it's declared; update handleClear to keep its current logic
(onSearchDebounced?.cancel(), setSearchTerm(""), onSearch("")) but ensure
onSearchDebounced is defined earlier and that the dependency array on the
useMemo remains [onSearch, debounced].

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7d95058-bab2-4ca5-a9a3-453a7069873f

📥 Commits

Reviewing files that changed from the base of the PR and between 39bac29 and 0dd6a00.

📒 Files selected for processing (1)
  • src/components/mui/search-input.js

Signed-off-by: Tomás Castillo <tcastilloboireau@gmail.com>
@tomrndom tomrndom requested a review from smarcet April 2, 2026 19:52
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.

1 participant