🥊

Reactでキーワードサジェストを作る

2023/04/20に公開
1

はじめに

こんにちは!
スペースマーケットでフロントエンドエンジニアをしているk___0122です。

過去に以下のようなキーワードサジェストを実装したので、Reactを使ってキーワードサジェストを実装する方法を簡単にですが紹介します。

具体的には、以下2つについて説明します。

  1. ユーザーが入力を終えてから一定時間経ってからリクエストを送信する方法
  2. キーボードで候補を選択できるようにする方法

ユーザーが入力を終えて一定時間経ってからリクエストを送信する

ユーザーが入力した文字をそのままリクエストに使うと、リクエストが連続して発生するため、サーバーに負荷をかける原因になります。
そこで、一定時間(例えば、500ミリ秒)空けた後にリクエストを送信するようにすることで、負荷を軽減できます。

今回はreact-useのuseDebounceというカスタムフックを使います。
useDebounceは第二引数に指定したミリ秒後に第一引数のコールバック関数を実行します。
https://github.com/streamich/react-use/blob/master/docs/useDebounce.md

以下はuseDebounceを使った実装になります。

AutocompleteDemo.tsx
import React, { useState } from 'react';
import { useDebounce } from 'react-use';

export const AutocompleteDemo = () => {
  const [keyword, setKeyword] = useState('');
  const [suggestions, setSuggestions] = useState<string[]>([]);

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setKeyword(event.target.value);
  };

  // keywordの値が変更されたら500ms後にfetchSuggestionsを呼び出す
  useDebounce(
    () => {
      // APIを呼び出す
      const fetchSuggestions = async () => {
        const response = await fetch('https://dummyapi.example.com');
        const data = await response.json();
        setSuggestions(data);
      };
      fetchSuggestions();
    },
    500,
    [keyword]
  );

  return (
    <>
      <input type="search" value={keyword} onChange={handleInputChange} />
      <ul>
        {suggestions.map((suggestion) => (
          <li key={suggestion}>{suggestion}</li>
        ))}
      </ul>
    </>
  );
};

キーボードで操作できるようにする

サジェストはクリックのみではなく、キーボードで候補を選択できるようにしました。
具体的には、以下のような手順で実装します。

  • キーボードの上下キーでカーソルを移動する
  • Escapeを押すとサジェストが閉じる
  • Enterで選択したキーワードを入力欄にセットしAPIを叩く

以下はkeyDownの処理を追加したものになります。

AutocompleteDemo.tsx
  const [keyword, setKeyword] = useState("");
  const [suggestions, setSuggestions] = useState<string[]>([]);
  const [focusedIndex, setFocusedIndex] = useState(-1);
  
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setKeyword(event.target.value);
  };

 // ...

  const handleOnKeyDown = (
    e: React.KeyboardEvent<HTMLInputElement>
  ) => {
    const eventKey =
      e.keyCode === 13 ? "ConfirmEnter" : e.key;
      if (
        eventKey === 'ArrowUp' ||
        eventKey === 'ArrowDown' ||
        eventKey === 'ConfirmEnter' ||
        eventKey === 'Escape'
      ) {
        e.preventDefault()
      }

    switch (eventKey) {
      case "ArrowUp":
        setFocusedIndex((prevIndex) =>
          prevIndex <= 0 ? suggestions.length - 1 : prevIndex - 1
        );
        break;

      case "ArrowDown":
        setFocusedIndex((prevIndex) =>
          prevIndex === suggestions.length - 1 ? 0 : prevIndex + 1
        );
        break;

      case "ConfirmEnter":
        // ここで検索のAPIを叩く
        break;

      case "Escape":
	setSuggestions([]);
        setFocusedIndex(-1);
        break;
    }
  };
  
  // keywordの値が変更されたら500ms後にfetchSuggestionsを呼び出す
  useDebounce(
    // ...
  );
  
  // ...
  
    return (
      <input
        type="search"
        value={keyword}
        onChange={handleInputChange}
        onKeyDown={handleOnKeyDown}
      />

SafariのみIME確定時のEnterとAPIを叩く際のEnterがうまく制御できなかったので、keyCodeで判断しました。
IME確定時のEnterの制御について過去にこちらでまとめているのでよかったらご覧ください!

最後に

スペースマーケットでは、一緒にサービスを成長させていく仲間を探しています。
とりあえずどんなことをしているのか聞いてみたいという方も大歓迎です!
ご興味ありましたら是非ご覧ください!
https://www.wantedly.com/projects/1113570
https://www.wantedly.com/projects/1113544
https://www.wantedly.com/projects/1061116
https://spacemarket.co.jp/recruit/engineer/

スペースマーケット Engineer Blog

Discussion

nap5nap5

キーボードの上下キーでカーソルを移動する

ここの実装は横展が効くようにcycle関数などにしてもありかなと思いました。

function cycle(start: number, end: number, value: number) {
  if (value < start) return end;
  if (value > end) return start;
  return value;
}

キー操作ではなく、ボタン駆動ですが、demo code.

https://stackblitz.com/edit/vitejs-vite-85p9yf?file=src%2FCowboy.tsx