React 编码挑战:TypeHead
Source: Dev.to
抱歉,我无法直接访问外部链接获取文章内容。请您提供需要翻译的文本,我将为您翻译成简体中文。
自动补全
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
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
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 (
Search for users
setQuery(e.target.value)}
placeholder={placeholder}
aria-autocomplete="list"
aria-controls="suggestions-list"
aria-expanded={suggestions.length > 0}
role="combobox"
className="typeahead-input"
/>
{isLoading && Loading...}
{error && (
{error}
)}
{suggestions.length > 0 && (
{suggestions.map((suggestion, index) => (
handleSuggestionClick(suggestion)}
tabIndex={-1}
>
{suggestion}
))}
)}
);
};
export default Typeahead;
CSS
.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;
}
关键更改和说明
-
一次性获取所有用户 – 组件在挂载时从
https://jsonplaceholder.typicode.com/posts获取完整的帖子列表,并将标题存入allUsers。(如果想要真实用户,请将 URL 替换为/users。) -
本地过滤 – 不再在每次键入时发起请求,组件使用
Array.filter在已获取的标题中本地过滤,匹配查询时不区分大小写。 -
性能 – 对于小数据集(例如 JSONPlaceholder 返回的 100 条帖子),这种方式更快且减少网络流量。对于非常大的数据集,可以加入记忆化(
useMemo)或服务器端分页。 -
可访问性与 UI – 键盘导航、ARIA 属性和样式保持不变。可以继续使用上面展示的
styles.css。
arlier.
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.