Conversation
The Snippet Manager list and the ATW Collect pane's snippet picker each
carried the same debounce: a `useEffect` opening a `setTimeout`, clearing
it on cleanup, against its own locally-declared 300ms constant. Two
copies means a change to the window has to be found and applied twice,
and the second copy is the one that gets missed.
`useDebouncedValue(value, delayMs)` and the shared `SEARCH_DEBOUNCE_MS`
now live in `@sep/framework`, and both consumers call it. The primitive
seeds its state from the incoming value rather than from a blank, so a
list mounted with a term already in state queries for that term instead
of fetching the unfiltered page first; both consumers mount with an empty
box today, so nothing user-visible moves. Callers trim at the call site,
which is where the old effects trimmed too, only inside the timer.
Behaviour is otherwise unchanged: same window, same one-publish-per-pause
coalescing, same clear on unmount.
`SnippetsListPage.test.tsx` moves from a full `vi.mock('@sep/framework')`
to an `importOriginal` partial mock so the page exercises the real
primitive; only the download hook stays stubbed. The ATW suite needed no
change.
Scope note: the ticket also asked for a shared snippets-search query
hook. SEP-1821 (#1349) has since moved ATW's search onto ATW's own
router, dropping the `approval` param and the row projection, and
`useSnippets` has gained a `sort` param, so the two hooks no longer share
an endpoint, a param surface, or a mapper. The only remaining overlap is
`apiClient.get` -> `normalizeAppListResponse` -> `keepPreviousData`,
which `useTasksList` shares equally: a generic app-list wrapper, not a
snippets-search one. Left for a follow-up rather than built here.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR centralizes the previously duplicated “search debounce” logic into @sep/framework by introducing a shared useDebouncedValue(value, delayMs) hook and a shared SEARCH_DEBOUNCE_MS constant, then updating both the Snippet Manager list and ATW Collect pane snippet picker to use the shared primitive.
Changes:
- Added
useDebouncedValueandSEARCH_DEBOUNCE_MSto@sep/frameworkand exported them via the hooks barrel and package entrypoint. - Replaced local
useEffect + setTimeoutdebounce blocks in@sep/snippetsand@sep/atwwithuseDebouncedValue(search.trim()). - Added unit tests in
@sep/frameworkcovering initial value behavior, timer coalescing, delay changes, and unmount cleanup; adjustedSnippetsListPagetests to partial-mock@sep/frameworkso the real debounce primitive is exercised.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/packages/framework/src/index.ts | Re-exports the new hook and constant from the framework package entrypoint. |
| frontend/packages/framework/src/hooks/useDebouncedValue.ts | Introduces the shared debounce hook and shared SEARCH_DEBOUNCE_MS constant. |
| frontend/packages/framework/src/hooks/useDebouncedValue.test.tsx | Adds unit tests validating debounce timing and cleanup semantics. |
| frontend/packages/framework/src/hooks/index.ts | Exports the new hook/constant from the hooks barrel. |
| frontend/packages/apps/snippets/src/SnippetsListPage.tsx | Replaces the inline debounce effect with useDebouncedValue(search.trim()). |
| frontend/packages/apps/snippets/src/SnippetsListPage.test.tsx | Switches to a partial @sep/framework mock so useDebouncedValue runs as real code while keeping the download hook stubbed. |
| frontend/packages/apps/atw/src/CollectPane.tsx | Replaces the inline debounce effect with useDebouncedValue(searchInput.trim()). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
yyyyyyyan
approved these changes
Aug 19, 2026
yyyyyyyan
enabled auto-merge (squash)
August 19, 2026 23:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The Snippet Manager list and the ATW Collect pane's snippet picker each carried the same debounce: a
useEffectopening asetTimeout, clearing it on cleanup, against its own locally-declared 300ms constant. Any change to the window had to be found and applied twice.useDebouncedValue(value, delayMs)and the sharedSEARCH_DEBOUNCE_MSnow live in@sep/framework, and both consumers call it.packages/framework/src/hooks/useDebouncedValue.ts(new) — the primitive plus the shared constant, exported from both barrels.packages/apps/snippets/src/SnippetsListPage.tsx— inline block and localSEARCH_DEBOUNCE_MSdropped.packages/apps/atw/src/CollectPane.tsx— inline block and localSNIPPET_SEARCH_DEBOUNCE_MSdropped.Two deliberate, non-user-visible details:
Otherwise identical: same window, same one-publish-per-pause coalescing, same clear on unmount.
Scope note: the search-hook half
The ticket also asked for a shared snippets-search query hook, on the premise that
useAtwSnippetSearchanduseSnippetswere two copies of one query. That premise no longer holds onmain. SEP-1821 (#1349) moved ATW's search onto ATW's own router: it now targets/apps/atw/snippets/rather thanSNIPPETS_APPS_API_BASE, sends noapprovalparam (pinned server-side), and has no row projection (toAtwSnippetSummarywas deleted as an identity map).useSnippetshas meanwhile gained asortparam.The two hooks therefore share no endpoint, no param surface, and no mapper. The only remaining overlap is
apiClient.get->normalizeAppListResponse->keepPreviousData— whichuseTasksListshares equally. That is a generic app-list wrapper, not a snippets-search one, and worth its own ticket rather than being smuggled in under this name. The ticket's query-key invariants are moot as a result, but hold trivially: neither hook was touched, so the Snippet Manager's key shape is byte-identical and no ATW result lands under['snippets', 'list'].Acceptance criteria
@sep/frameworkexportsuseDebouncedValueplus the shared 300ms constant, replacing both local copies.@sep/frameworkcarries unit tests for the new export, covering timer behaviour and cleanup on unmount.@sep/snippetsand@sep/atwsuites pass with no behavioural edits — the only change isSnippetsListPage.test.tsxmoving from a fullvi.mock('@sep/framework')to animportOriginalpartial mock, so the page exercises the real primitive while the download hook stays stubbed. The ATW suite needed no change at all.hooks.test.tsxpasses unedited (useSnippetswas not touched).ScriptPreviewField.tsx's ownDEBOUNCE_MSis left alone as the ticket directs: it debounces an effect withAbortControllercancellation, not a value.Test plan
pnpm --filter @sep/framework test— 762 pass, including the 6 newuseDebouncedValuespecs.pnpm --filter @sep/snippets test— 72 pass.pnpm --filter @sep/atw test— 83 pass.pnpm --filter @sep/framework --filter @sep/snippets --filter @sep/atw type-check— clean.oxlint— 0 errors on the full frontend.Manually: type in the Snippet Manager search box and in the Collect pane's snippet picker; each fires one request per pause, not one per keystroke.