TypeHead
import { useState, useEffect, useRef } from "react";
const Typeahead = ({ placeholder = "Search..." }) => {
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState([]);
const [allUsers, setAllUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [activeSuggestion, setActiveSuggestion] = useState(-1);
const inputRef = useRef(null);
// Fetch all users once on component mount
useEffect(() => {
const fetchAllUsers = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts`
);
if (!response.ok) throw new Error("Failed to fetch users");
const data = await response.json();
console.log(data);
setAllUsers(data.map((user) => user.title));
} catch (err) {
setError(err.message);
setAllUsers([]);
} finally {
setIsLoading(false);
}
};
fetchAllUsers();
}, []);
// Filter suggestions locally based on query
useEffect(() => {
if (query.length < 1) {
setSuggestions([]);
setActiveSuggestion(-1);
return;
}
const filteredSuggestions = allUsers.filter((title) =>
title.toLowerCase().includes(query.toLowerCase())
);
setSuggestions(filteredSuggestions);
setActiveSuggestion(-1);
}, [query, allUsers]);
// Handle keyboard navigation for accessibility
const handleKeyDown = (e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setActiveSuggestion((prev) =>
prev < suggestions.length - 1 ? prev + 1 : 0
);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActiveSuggestion((prev) =>
prev > 0 ? prev - 1 : suggestions.length - 1
);
} else if (e.key === "Enter" && activeSuggestion >= 0) {
e.preventDefault();
setQuery(suggestions[activeSuggestion]);
setSuggestions([]);
setActiveSuggestion(-1);
}
};
// Handle suggestion click
const handleSuggestionClick = (suggestion) => {
setQuery(suggestion);
setSuggestions([]);
setActiveSuggestion(-1);
inputRef.current.focus();
};
return (
<div className="typeahead-container" onKeyDown={handleKeyDown}>
<label htmlFor="typeahead-input" className="sr-only">
Search for users
</label>
<input
id="typeahead-input"
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
aria-autocomplete="list"
aria-controls="suggestions-list"
aria-expanded={suggestions.length > 0}
role="combobox"
className="typeahead-input"
/>
{isLoading && <div className="loading">Loading...</div>}
{error && (
<div className="error" role="alert">
{error}
</div>
)}
{suggestions.length > 0 && (
<ul id="suggestions-list" role="listbox" className="suggestions-list">
{suggestions.map((suggestion, index) => (
<li
key={suggestion}
role="option"
aria-selected={index === activeSuggestion}
className={`suggestion-item ${
index === activeSuggestion ? "active" : ""
}`}
onClick={() => handleSuggestionClick(suggestion)}
tabIndex={-1}
>
{suggestion}
</li>
))}
</ul>
)}
</div>
);
};
export default Typeahead;
.typeahead-container {
position: relative;
width: 300px;
margin: 20px auto;
}
.typeahead-input {
width: 100%;
padding: 8px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
}
.suggestions-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
list-style: none;
padding: 0;
margin: 4px 0 0 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.suggestion-item {
padding: 8px;
cursor: pointer;
}
.suggestion-item:hover,
.suggestion-item.active {
background-color: #f0f0f0;
}
.loading,
.error {
padding: 8px;
color: #666;
font-size: 14px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
Key Changes and Explanation:
Fetch All Users Once: The code now fetches the complete list of users from
https://jsonplaceholder.typicode.com/userswhen the component mounts, storing it in theallUsersstate.Local Filtering: Instead of querying the API with each input change, the filtering is done locally using JavaScript's
filtermethod. It checks if the user's name includes the query string (case-insensitive).Performance: Local filtering is faster for small datasets like this one (10 users in jsonplaceholder). For very large datasets, you might want to consider other optimization techniques like memoization with
useMemo, though it's not necessary here.Accessibility and UI: The accessibility features, keyboard navigation, and styling remain unchanged from the previous version. You can still use the same
styles.cssprovided earlier.
Notes:
- The local filtering is case-insensitive for better user experience (
toLowerCase()). - If the API fetch fails, an error message is displayed, and the typeahead will not function until the data is loaded.
- For real-world applications with larger datasets, consider implementing pagination or a more efficient search algorithm if performance becomes an issue.
Top comments (0)