Skip to content

fix: debounce URL navigation in skills search to reduce input lag#587

Open
neeravmakwana wants to merge 1 commit intoopenclaw:mainfrom
neeravmakwana:fix/skills-search-input-lag
Open

fix: debounce URL navigation in skills search to reduce input lag#587
neeravmakwana wants to merge 1 commit intoopenclaw:mainfrom
neeravmakwana:fix/skills-search-input-lag

Conversation

@neeravmakwana
Copy link

@neeravmakwana neeravmakwana commented Mar 3, 2026

Summary

  • Debounce the navigate() call in onQueryChange (skills browse page) so the URL is not updated on every keystroke
  • setQuery() remains immediate so the controlled input stays responsive
  • Uses 220 ms delay, matching the existing search-action debounce in the same useEffect

Problem

Typing in the skills search input on /skills causes noticeable lag. Each keystroke calls navigate({ search: … }) synchronously, which triggers history.replaceState, TanStack Router re-evaluation, and useSearch() invalidation — resulting in multiple React re-render cycles per keystroke on top of the setQuery render.

The Convex search action was already debounced at 220 ms, but the navigate call was not.

Test plan

  • Type rapidly in the skills search input — input should feel responsive with no lag
  • After typing stops, URL ?q= param updates within ~220 ms
  • Clearing the input removes the ?q param from the URL
  • Sharing/bookmarking a URL with ?q=term still pre-populates the search on load
  • Browser back/forward with different ?q values still syncs the input
  • bun run lint — 0 warnings, 0 errors
  • bun run test — 604 tests pass

Made with Cursor

@vercel
Copy link
Contributor

vercel bot commented Mar 3, 2026

@neeravmakwana is attempting to deploy a commit to the Amantus Machina Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

@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: 3bf6ccaf62

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

})
window.clearTimeout(navigateTimer.current)
const trimmed = next.trim()
navigateTimer.current = window.setTimeout(() => {

Choose a reason for hiding this comment

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

P2 Badge Cancel pending query navigation on external URL changes

Scheduling navigate() in onQueryChange without any cancellation path other than another keystroke means a stale timer can overwrite user-driven navigation on the same page. If a user types and then immediately uses browser back/forward (or any other search-param navigation) within 220ms, the queued callback still runs and replaces the URL with the old q, effectively undoing that navigation and re-syncing the input to stale state.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 3, 2026

Greptile Summary

This PR debounces the navigate() call inside onQueryChange in the skills browse model, matching the existing 220 ms debounce already applied to the Convex search action. setQuery is intentionally kept synchronous so the controlled input remains responsive. The implementation is clean and the approach is correct.

Key observations:

  • The navigateTimer ref with window.setTimeout / window.clearTimeout is idiomatic and avoids adding a third-party debounce dependency.
  • Unmount cleanup (useEffect(() => …, [])) correctly prevents a stale navigation after the component is removed.
  • One logic gap: the existing useEffect that syncs query when search.q changes externally (browser back/forward, programmatic navigation) does not cancel the in-flight timer, so a pending debounce can overwrite the external URL change after ~220 ms. Adding window.clearTimeout(navigateTimer.current) to that effect would close this gap.

Confidence Score: 3/5

  • Mostly safe to merge; one logic gap where a browser back/forward action during the debounce window can be silently overwritten.
  • The core debounce approach is correct and well-scoped to a single file. The unmount cleanup is properly handled. However, the missing clearTimeout in the search.q sync effect introduces a real (if rare) race condition with external URL changes that could confuse users relying on browser history navigation.
  • src/routes/skills/-useSkillsBrowseModel.ts — specifically lines 76–78 (the search.q sync effect) which should also cancel the pending timer.

Last reviewed commit: 3bf6cca

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +221 to +223
useEffect(() => {
return () => window.clearTimeout(navigateTimer.current)
}, [])
Copy link
Contributor

Choose a reason for hiding this comment

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

Pending timer not cancelled on external URL change

The useEffect on line 76–78 syncs query from search.q when the URL changes externally (e.g. browser back/forward, a direct link). However, the pending navigateTimer is not cleared at that point. This means:

  1. User types "foo" → setQuery("foo") fires immediately, 220 ms timer starts.
  2. User presses browser back before the timer fires → search.q reverts (or becomes undefined), and the useEffect correctly resets query.
  3. After 220 ms the pending timer fires and calls navigate({ q: "foo" }), overwriting the browser-back navigation.

The fix is to clear the timer in the same effect that responds to external URL changes:

Suggested change
useEffect(() => {
return () => window.clearTimeout(navigateTimer.current)
}, [])
useEffect(() => {
window.clearTimeout(navigateTimer.current)
setQuery(search.q ?? '')
}, [search.q])

This ensures any in-flight debounce is cancelled whenever the URL is updated from outside the component (back/forward, programmatic navigation, direct link load).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/routes/skills/-useSkillsBrowseModel.ts
Line: 221-223

Comment:
**Pending timer not cancelled on external URL change**

The `useEffect` on line 76–78 syncs `query` from `search.q` when the URL changes externally (e.g. browser back/forward, a direct link). However, the pending `navigateTimer` is not cleared at that point. This means:

1. User types "foo" → `setQuery("foo")` fires immediately, 220 ms timer starts.
2. User presses browser **back** before the timer fires → `search.q` reverts (or becomes `undefined`), and the `useEffect` correctly resets `query`.
3. After 220 ms the pending timer fires and calls `navigate({ q: "foo" })`, **overwriting the browser-back** navigation.

The fix is to clear the timer in the same effect that responds to external URL changes:

```suggestion
  useEffect(() => {
    window.clearTimeout(navigateTimer.current)
    setQuery(search.q ?? '')
  }, [search.q])
```

This ensures any in-flight debounce is cancelled whenever the URL is updated from outside the component (back/forward, programmatic navigation, direct link load).

How can I resolve this? If you propose a fix, please make it concise.

onQueryChange called navigate() on every keystroke to sync the query
to the URL. Each navigate triggers history.replaceState, TanStack
Router re-evaluation, and useSearch() invalidation, causing multiple
re-renders per keystroke.

Keep setQuery() immediate so the controlled input stays responsive,
but debounce the navigate() call at 220 ms (matching the existing
search-action debounce). Cancel the pending timer when search.q
changes externally (browser back/forward) to prevent the debounced
navigate from overwriting the external URL change.

Made-with: Cursor
@neeravmakwana neeravmakwana force-pushed the fix/skills-search-input-lag branch from 3bf6cca to ec3e1b7 Compare March 3, 2026 03:20
@neeravmakwana
Copy link
Author

Updated — addressed the race condition identified in review: the search.q sync effect now cancels any pending debounce timer (window.clearTimeout(navigateTimer.current)) before setting the query from the external URL. This prevents a stale debounced navigate() from overwriting browser back/forward navigation.

Also moved the navigateTimer ref declaration up alongside the other refs for cleaner ordering.

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