React Coding Challenge : TypeHead

Published: (January 17, 2026 at 09:39 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

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 
      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;
}

Key Changes and Explanation

  • Fetch All Users Once – The component now retrieves the full list of posts from https://jsonplaceholder.typicode.com/posts when it mounts and stores the titles in allUsers. (If you prefer real users, replace the URL with /users.)

  • Local Filtering – Instead of making a request on every keystroke, the component filters the already‑fetched titles locally with Array.filter, matching the query case‑insensitively.

  • Performance – For small data sets (e.g., the 100 posts returned by JSONPlaceholder) this approach is faster and reduces network traffic. For very large data sets you could add memoization (useMemo) or server‑side pagination.

  • Accessibility & UI – Keyboard navigation, ARIA attributes, and styling remain unchanged. The same styles.css can be used as shown above.

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.
Back to Blog

Related posts

Read more »