|
1 | | -import { useState, useEffect, useRef } from "react" |
| 1 | +import { useState, useEffect, useRef, useMemo } from "react" |
2 | 2 | import "./Search.css" |
3 | 3 | import { clsx } from "~/lib/clsx/clsx.ts" |
4 | 4 | import { useClickOutside } from "~/hooks/useClickOutside.tsx" |
@@ -43,34 +43,49 @@ interface SearchProps { |
43 | 43 |
|
44 | 44 | function Search({ chains, tokens, small, environment, lanes }: SearchProps) { |
45 | 45 | const [search, setSearch] = useState("") |
| 46 | + const [debouncedSearch, setDebouncedSearch] = useState("") |
46 | 47 | const [openSearchMenu, setOpenSearchMenu] = useState(false) |
47 | 48 | const [isActive, setIsActive] = useState(false) |
48 | | - const [networksResults, setNetworksResults] = useState<SearchProps["chains"]>([]) |
49 | | - const [tokensResults, setTokensResults] = useState<SearchProps["tokens"]>([]) |
50 | | - const [lanesResults, setLanesResults] = useState<SearchProps["lanes"]>([]) |
51 | 49 | const searchRef = useRef<HTMLDivElement>(null) |
52 | 50 |
|
| 51 | + // Debounce search input |
53 | 52 | useEffect(() => { |
54 | | - if (search) { |
55 | | - const networks = chains.filter((chain) => chain.name.toLowerCase().includes(search.toLowerCase())) |
56 | | - const tokensList = tokens.filter((token) => token.id.toLowerCase().includes(search.toLowerCase())) |
57 | | - const lanesList = lanes.filter( |
58 | | - (lane) => |
59 | | - (lane.sourceNetwork.name.toLowerCase().includes(search.toLowerCase()) || |
60 | | - lane.destinationNetwork.name.toLowerCase().includes(search.toLowerCase())) && |
61 | | - (lane.lane.supportedTokens ? Object.keys(lane.lane.supportedTokens).length : 0) > 0 |
62 | | - ) |
63 | | - setNetworksResults(networks) |
64 | | - setTokensResults(tokensList) |
65 | | - setLanesResults(lanesList) |
| 53 | + const timer = setTimeout(() => { |
| 54 | + setDebouncedSearch(search) |
| 55 | + }, 300) |
| 56 | + |
| 57 | + return () => clearTimeout(timer) |
| 58 | + }, [search]) |
| 59 | + |
| 60 | + // Memoize filtered results to prevent unnecessary recalculations |
| 61 | + const networksResults = useMemo(() => { |
| 62 | + if (!debouncedSearch) return [] |
| 63 | + return chains.filter((chain) => chain.name.toLowerCase().includes(debouncedSearch.toLowerCase())) |
| 64 | + }, [debouncedSearch, chains]) |
| 65 | + |
| 66 | + const tokensResults = useMemo(() => { |
| 67 | + if (!debouncedSearch) return [] |
| 68 | + return tokens.filter((token) => token.id.toLowerCase().includes(debouncedSearch.toLowerCase())) |
| 69 | + }, [debouncedSearch, tokens]) |
| 70 | + |
| 71 | + const lanesResults = useMemo(() => { |
| 72 | + if (!debouncedSearch) return [] |
| 73 | + return lanes.filter( |
| 74 | + (lane) => |
| 75 | + (lane.sourceNetwork.name.toLowerCase().includes(debouncedSearch.toLowerCase()) || |
| 76 | + lane.destinationNetwork.name.toLowerCase().includes(debouncedSearch.toLowerCase())) && |
| 77 | + (lane.lane.supportedTokens ? Object.keys(lane.lane.supportedTokens).length : 0) > 0 |
| 78 | + ) |
| 79 | + }, [debouncedSearch, lanes]) |
| 80 | + |
| 81 | + // Handle menu visibility |
| 82 | + useEffect(() => { |
| 83 | + if (debouncedSearch) { |
66 | 84 | setOpenSearchMenu(true) |
67 | 85 | } else { |
68 | | - setNetworksResults([]) |
69 | | - setTokensResults([]) |
70 | | - setLanesResults([]) |
71 | 86 | setOpenSearchMenu(false) |
72 | 87 | } |
73 | | - }, [search, chains, tokens]) |
| 88 | + }, [debouncedSearch]) |
74 | 89 |
|
75 | 90 | useClickOutside(searchRef, () => setOpenSearchMenu(false)) |
76 | 91 |
|
@@ -126,6 +141,7 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { |
126 | 141 | <img |
127 | 142 | src={network.logo} |
128 | 143 | alt="" |
| 144 | + loading="lazy" |
129 | 145 | onError={({ currentTarget }) => { |
130 | 146 | currentTarget.onerror = null // prevents looping |
131 | 147 | currentTarget.src = fallbackTokenIconUrl |
@@ -154,6 +170,7 @@ function Search({ chains, tokens, small, environment, lanes }: SearchProps) { |
154 | 170 | <img |
155 | 171 | src={token.logo} |
156 | 172 | alt="" |
| 173 | + loading="lazy" |
157 | 174 | onError={({ currentTarget }) => { |
158 | 175 | currentTarget.onerror = null // prevents looping |
159 | 176 | currentTarget.src = fallbackTokenIconUrl |
|
0 commit comments