DEV Community

ZeeshanAli-0704
ZeeshanAli-0704

Posted on

React Coding Challenge : TypeHead

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;

Enter fullscreen mode Exit fullscreen mode
.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;
}


Enter fullscreen mode Exit fullscreen mode

Key Changes and Explanation:

  1. Fetch All Users Once: The code now fetches the complete list of users from https://jsonplaceholder.typicode.com/users when the component mounts, storing it in the allUsers state.

  2. Local Filtering: Instead of querying the API with each input change, the filtering is done locally using JavaScript's filter method. It checks if the user's name includes the query string (case-insensitive).

  3. 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.

  4. Accessibility and UI: The accessibility features, keyboard navigation, and styling remain unchanged from the previous version. You can still use the same styles.css provided 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)