Blog

React – Build Your Own Auto Suggest Component

Creating an Advanced Auto-Suggest Component in React

In this article, we’ll build an advanced auto-suggest component in React. This component will cover several important features:

  1. Passing static data.
  2. Fetching data from an API.
  3. Custom item rendering for styling.
  4. Keyboard navigation.
  5. List item event notifiers.

Let’s dive into the step-by-step implementation.

Step 1: Setting Up the Initial Data

We’ll start by defining some static data to use in our auto-suggest component.

const data = [
{ term: 'India', icon: '🚣' },
{ term: 'Indonesia', icon: '⛰️' },
{ term: 'Iceland', icon: '🏕️' },
{ term: 'US', icon: '🏖️' },
{ term: 'UK', icon: '🏘️' },
];

This data array will be used for static auto-suggestions. Each object in the array represents a suggestion with a term and an associated icon.

Step 2: Implementing the Throttle Function

We’ll use a throttle function to limit the rate at which our event handler can be executed. This helps to prevent excessive function calls, especially when handling input events.

const throttle = (func, limit) => {
  let lastFunc;
  let lastRan;
  return function() {
    const context = this;
    const args = arguments;
    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function() {
        if ((Date.now() - lastRan) >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
};

The throttle function ensures that the provided func is not called more frequently than the specified limit. It uses a combination of setTimeout and timestamp checks to control the function execution.

Step 3: Creating the List Component

The List component renders the list of suggestions based on the input.

function List({ data, show, onClick, getDisplayValue, renderItem, currentItemIndex, erefs, listRef }) {
  if (!show) return null;
  if (!Array.isArray(data)) return <></>;
  return (
    <ul ref={listRef} className="search-list" role="group">
      {data.map((item, index) => {
        let style = index === currentItemIndex ? "list-item-current" : "";
        return (
          <li key={index} className={style} role="menu-item" onClick={(e) => onClick(e, item)}>
            <a href="#" ref={(r) => { erefs[index] = r; }}>
              {renderItem ? renderItem(item) : getDisplayValue(item)}
            </a>
          </li>
        );
      })}
    </ul>
  );
}

This component receives several props:

  • data: The list of items to display.
  • show: A boolean to control the visibility of the list.
  • onClick: A function to handle item clicks.
  • getDisplayValue: A function to get the display value of an item.
  • renderItem: A function to render a custom item.
  • currentItemIndex: The index of the currently focused item.
  • erefs: An array of references to the list items.
  • listRef: A reference to the list container.

The component iterates over the data array and renders each item as a list element (<li>). If the renderItem function is provided, it uses it to render the item. Otherwise, it uses the getDisplayValue function.

Step 4: Creating the AutoComplete Component

The AutoComplete component manages the input state, handles keyboard navigation, and displays the suggestion list.

function AutoComplete({ value, items = [], placeholder, onChange, onSelect, getDisplayValue, renderItem, shouldItemRender }) {
  const [term, setTerm] = useState('');
  const [result, setResult] = useState([]);
  const [isSelected, toggleSelect] = useState(true);
  const [itemIndex, setItemIndex] = useState(-1);
  
  const compRef = useRef(); // Reference to autocomplete component
  const inputRef = useRef(); // Reference to input element
  const listRef = useRef(); // Reference to container of autocomplete list
  const refs = useRef([]);
  
  const onChangeDelayed = throttle(onChange, 100);
  
  useEffect(() => {
    document.addEventListener("click", (e) => {
      if (!compRef.current.contains(e.target)) {
        toggleSelect(true);
      }
    });
  }, []);
  
  useEffect(() => {
    if (!isSelected) showResult();
  }, [value, items]);
  
  useEffect(() => {
    let list = refs.current[itemIndex] || null;
    if (list) list.focus();
  }, [itemIndex]);
  
  function handleChange(e) {
    setTerm(e.target.value);
    onChangeDelayed(e, e.target.value);
    toggleSelect(false);
  }

  function showResult() {
    if (term.trim() === '') {
      toggleSelect(true);
      return;
    }
    if (!items.length) {
      setResult([]);
      return;
    }
    setResult(shouldItemRender ? items.filter((item) => shouldItemRender(item, term)) : items);
  }
  
  function onClick(e, item) {
    setTerm(getDisplayValue(item));
    toggleSelect(true);
    onSelect(item);
    setItemIndex(-1);
    inputRef.current.focus();
  }
  
  function handleKeyDown(e) {
    switch(e.which) {
      case 38: // up
        if (itemIndex > 0) setItemIndex((idx) => idx - 1);
        break;
      case 40: // down
      case 9: // tab
        if (itemIndex < items.length - 1) {
          setItemIndex((idx) => idx + 1);
        }
        break;
      case 13: // enter
        onClick(e, items[itemIndex]);
        break;
      case 27: // esc
        setItemIndex(-1);
        toggleSelect(true);
        inputRef.current.focus();
        break;
    }
  }
  
  return (
    <div className="ux-autocomplete" ref={compRef} onKeyDown={handleKeyDown}>
      <input type="text" ref={inputRef} value={term} onFocus={() => setItemIndex(-1)} onChange={handleChange} placeholder={placeholder} />
      <List show={!isSelected} tabIndex={1000} erefs={refs.current} listRef={listRef} currentItemIndex={itemIndex} data={result} getDisplayValue={getDisplayValue} renderItem={renderItem} onClick={onClick} />
    </div>
  );
}

This component receives several props:

  • value: The current value of the input field.
  • items: The list of items to display.
  • placeholder: The placeholder text for the input field.
  • onChange: A function to handle input changes.
  • onSelect: A function to handle item selection.
  • getDisplayValue: A function to get the display value of an item.
  • renderItem: A function to render a custom item.
  • shouldItemRender: A function to filter the items based on the input value.

The AutoComplete component manages several states:

  • term: The current input value.
  • result: The list of filtered items to display.
  • isSelected: A boolean to control the visibility of the list.
  • itemIndex: The index of the currently focused item.

It also uses several references:

  • compRef: A reference to the autocomplete component.
  • inputRef: A reference to the input element.
  • listRef: A reference to the container of the autocomplete list.
  • refs: An array of references to the list items.

The component handles input changes, keyboard navigation, and item selection. It uses the throttle function to limit the rate of input change handling.

Step 5: Integrating AutoComplete in the Application

Finally, we integrate the AutoComplete component into our application and demonstrate using both static and remote data.

function App() {
  const [value, setValue] = useState('India');
  const [remoteValue, setRemoteValue] = useState('');
  const [remoteData, setRemoteData] = useState([]);
  
  const onChange = (e, value) => {
    setValue(value);
  };
  
  const onSelect = (value) => {
    setValue(value.term);
  };
  
  const remoteURL = `https://freetestapi.com/api/v1/countries?search=${remoteValue}`;
  
  useEffect(() => {
    if (!remoteValue) return;
    fetch(remoteURL, { mode: 'cors' })
      .then((resp) => resp.json())
      .then((json) => setRemoteData(json))
      .catch((error) => console.error('Request failed', error));
  }, [remoteValue]);
  
  const shouldItemRender = (item, value) => item.term.toLowerCase().includes(value.toLowerCase());
  
  return (
    <div>
      <div>
        <span>Static Data</span>
        <AutoComplete
          placeholder="enter search (type i or u)"
          getDisplayValue={(item) => item.term}
          renderItem={(item) => (
            <div className="search-list-item"><span>{item.icon}</span>{item.term}</div>
          )}
          items={data}
          onChange={onChange}
          onSelect={onSelect}
          shouldItemRender={shouldItemRender}
          value={value}
        />
      </div>
      <br/><br/>
      <div>
        <span>Async Data</span>
        <AutoComplete
          placeholder="enter search (type i or u)"
          getDisplayValue={(item) => item.name}
          renderItem={(item) => (
            <li className="search-list-item">{item.name}</li>
          )}
          items={remoteData}
          onSelect={(value) => setRemoteValue(value.name)}
          onChange={(e, value) => setRemoteValue(value)}
          value={remoteValue}
        />
      </div>
    </div>
  );
}

const rootElement = document.querySelector("#root");
ReactDOM.render(<App />, rootElement);

Conclusion

In this article, we created a React auto-suggest component that handles static and dynamic data, custom item rendering, keyboard navigation, and list item event notifications. By following these steps, you can build an advanced component that enhances user interaction in your applications.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply