55 useCallback ,
66 useEffect ,
77 useRef ,
8+ useMemo ,
89 type KeyboardEvent ,
910 type ChangeEvent ,
1011} from "react" ;
@@ -54,6 +55,7 @@ export function TagInput({
5455 const [ inputValue , setInputValue ] = useState ( "" ) ;
5556 const [ isOpen , setIsOpen ] = useState ( false ) ;
5657 const [ highlightedIndex , setHighlightedIndex ] = useState ( - 1 ) ;
58+ const [ lastSuggestionsLength , setLastSuggestionsLength ] = useState ( 0 ) ;
5759 const inputRef = useRef < HTMLInputElement > ( null ) ;
5860 const dropdownRef = useRef < HTMLDivElement > ( null ) ;
5961
@@ -69,10 +71,19 @@ export function TagInput({
6971 ) ;
7072
7173 // Filter out already selected tags
72- const suggestions : TagSuggestion [ ] =
73- searchResults ?. data ?. filter (
74- ( t ) => ! tags . includes ( t . title . toLowerCase ( ) ) ,
75- ) || [ ] ;
74+ const suggestions : TagSuggestion [ ] = useMemo (
75+ ( ) =>
76+ searchResults ?. data ?. filter (
77+ ( t ) => ! tags . includes ( t . title . toLowerCase ( ) ) ,
78+ ) || [ ] ,
79+ [ searchResults ?. data , tags ] ,
80+ ) ;
81+
82+ // Reset highlighted index when suggestions change (synchronous state derivation)
83+ if ( suggestions . length !== lastSuggestionsLength ) {
84+ setHighlightedIndex ( - 1 ) ;
85+ setLastSuggestionsLength ( suggestions . length ) ;
86+ }
7687
7788 const addTag = useCallback (
7889 ( tag : string ) => {
@@ -165,11 +176,6 @@ export function TagInput({
165176 return ( ) => document . removeEventListener ( "mousedown" , handleClickOutside ) ;
166177 } , [ ] ) ;
167178
168- // Reset highlighted index when suggestions change
169- useEffect ( ( ) => {
170- setHighlightedIndex ( - 1 ) ;
171- } , [ suggestions . length ] ) ;
172-
173179 const isMaxReached = tags . length >= maxTags ;
174180
175181 // Format post count for display
@@ -229,19 +235,19 @@ export function TagInput({
229235 onFocus = { ( ) => inputValue . length >= 1 && setIsOpen ( true ) }
230236 placeholder = { tags . length === 0 ? placeholder : "Add more..." }
231237 disabled = { disabled }
232- className = "min-w-[120px] w-full border-none bg-transparent px-1 py-1 text-sm text-neutral-900 placeholder:text-neutral-400 focus:outline-none focus:ring-0 dark:text-white dark:placeholder:text-neutral-500"
238+ className = "w-full min-w-[120px] border-none bg-transparent px-1 py-1 text-sm text-neutral-900 placeholder:text-neutral-400 focus:outline-none focus:ring-0 dark:text-white dark:placeholder:text-neutral-500"
233239 autoComplete = "off"
234240 />
235241
236242 { /* Autocomplete Dropdown */ }
237243 { isOpen && inputValue . length >= 1 && (
238244 < div
239245 ref = { dropdownRef }
240- className = "absolute left-0 top-full z-50 mt-1 w-72 max-h-64 overflow-y-auto rounded-lg border border-neutral-200 bg-white shadow-lg dark:border-neutral-700 dark:bg-neutral-800"
246+ className = "absolute left-0 top-full z-50 mt-1 max-h-64 w-72 overflow-y-auto rounded-lg border border-neutral-200 bg-white shadow-lg dark:border-neutral-700 dark:bg-neutral-800"
241247 >
242248 { isLoading ? (
243249 < div className = "flex items-center justify-center p-4 text-neutral-500" >
244- < Loader2 className = "h-4 w-4 animate-spin mr-2 " />
250+ < Loader2 className = "mr-2 h-4 w-4 animate-spin" />
245251 < span className = "text-sm" > Searching...</ span >
246252 </ div >
247253 ) : suggestions . length > 0 ? (
@@ -262,7 +268,7 @@ export function TagInput({
262268 { suggestion . title }
263269 </ span >
264270 < span
265- className = { `text-xs px-2 py-0.5 rounded-full ${
271+ className = { `rounded-full px-2 py-0.5 text-xs ${
266272 suggestion . postCount > 100
267273 ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
268274 : suggestion . postCount > 10
0 commit comments