dev TailwindCSS

UIライブラリを使わない custom autocomplete React 実装ガイド - TypeScriptで学ぶ完全版

custom autocomplete React(カスタムオートコンプリート)をTypeScriptとTailwind CSSで実装する方法を、AI assistant Claude との対話を通じて実践的に学びます。UIライブラリに頼らず、React Hooksの基礎から応用まで段階的に理解できる完全ガイドです。

読者がこの記事から得られる知識

この記事では、AI assistant Claude との対話を通じて学んだ custom autocomplete React の実装方法を共有します。実際の開発現場でよくある疑問やエラーに直面しながら、一つずつ解決していく過程を記録しました。

この記事で学べること:

  • custom autocomplete React をゼロから実装する具体的な手順
  • React Hooks(useState、useRef、useEffect)の実践的な使い方
  • TypeScript での型定義とエラー解決方法
  • Tailwind CSS を使った relative absolute の配置制御
  • キーボード操作とマウス操作の両方に対応したUI実装
  • DOM要素の参照と外側クリック検知の仕組み

実務で即使える実装パターンと、初学者が陥りやすいエラーの解決方法を理解できます。

今回ハンズオンした内容

AI assistant Claude との対話を通じて、custom autocomplete React component を TypeScript で実装しました。UIライブラリを使わずに一から作ることで、React の基礎概念を深く理解することができました。

プロジェクト構造

apps/web/
├── app/
│   ├── components/
│   │   ├── Header.tsx
│   │   └── AutoComplete.tsx
│   └── page.tsx
├── package.json
└── tsconfig.json

ステップ1: プロジェクトの準備とコンポーネント作成

まず、AutoComplete.tsx ファイルを作成し、基本構造を実装しました。

実行する操作:

AutoComplete.tsx ファイルを作成し、以下の内容を記述します。

"use client"

import { useState, useRef, useEffect } from "react";

const cities = [
  "Tokyo",
  "Osaka",
  "Nagoya",
  "Sapporo",
  "Fukuoka",
];

export default function AutoComplete () {
  const [isOpen, setIsOpen] = useState(false);
  const [filteredOptions, setFilteredOptions] = useState<string[]>([]);
  const [inputValue, setInputValue] = useState("");
  const [selectedIndex, setSelectedIndex] = useState(-1);

  const inputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLUListElement>(null);

  const handleInputChange = (e:React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setInputValue(value);

    if (value.trim() === "") {
      setFilteredOptions([]);
      setIsOpen(false);
    } else {
      const filtered = cities.filter((city) => 
        city.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredOptions(filtered)
      setIsOpen(true);
      setSelectedIndex(-1);
    }
  };

  const handleOptionClick = (option: string) => {
    setInputValue(option);
    setIsOpen(false);
    setFilteredOptions([]);
    setSelectedIndex(-1);
  };

  const handleKeyDown = (e:React.KeyboardEvent<HTMLInputElement>) => {
    if (!isOpen) return;

    switch (e.key) {
      case "ArrowDown": 
        e.preventDefault();
        setSelectedIndex((prev) => 
          prev < filteredOptions.length - 1 ? prev+1 : prev
        );
        break;
      case "ArrowUp": 
        e.preventDefault();
        setSelectedIndex((prev) => 
          prev > 0 ? prev-1 : 0
        );
        break;
      case "Enter": {
        e.preventDefault();
        const selectedOption = filteredOptions[selectedIndex];
        if (selectedIndex >= 0 && selectedOption) {
          handleOptionClick(selectedOption);
        }
        break;
      }
      case "Escape":
        setIsOpen(false);
        setSelectedIndex(-1); 
        break;
    }    
  };

  useEffect( () => {
    const handlecClickedOutside = (event: MouseEvent) => {
      if (
        inputRef.current
        && !inputRef.current.contains(event.target as Node)
        && listRef.current
        && !listRef.current.contains(event.target as Node)
      ) {
        setIsOpen(false);
      }
    };

    document.addEventListener("mousedown", handlecClickedOutside);
    return () => {
      document.removeEventListener("mousedown", handlecClickedOutside);
    };
  }, []);

  return (
    <div className="relative w-64">
      <div className="relative">
        <input 
          ref={inputRef}
          type="text"
          value={inputValue}
          onChange={handleInputChange}
          onKeyDown={handleKeyDown}
          placeholder="Search..."
          className="w-full pl-4 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
      </div>

      {isOpen && filteredOptions.length > 0 && (
        <ul
          ref={listRef}
          className="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-auto"
        >
          {filteredOptions.map((option, index) => (
            <li
              key={option}
              onClick={()=>handleOptionClick(option)}
              className={`px-4 py-2 cursor-pointer hover:bg-blue-100 ${
                index === selectedIndex ? "bg-blue-200" : ""
              }`}
            >
              {option}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

操作の意味:

custom autocomplete React component の基本構造を実装しています。主要な要素は以下の通りです。

useState で管理する状態:

  • isOpen: 候補リストの表示/非表示
  • filteredOptions: フィルタリングされた候補
  • inputValue: 入力フィールドの値
  • selectedIndex: キーボードで選択中の候補位置

useRef で管理するDOM参照:

  • inputRef: 入力フィールドへの参照
  • listRef: 候補リストへの参照

イベントハンドラ:

  • handleInputChange: 入力変更時の処理
  • handleOptionClick: 候補クリック時の処理
  • handleKeyDown: キーボード操作の処理

操作を実行する意図:

UIライブラリを使わずに custom autocomplete React を実装することで、React の状態管理とイベント処理の基礎を深く理解できます。特に、useState と useRef の使い分け、TypeScript の型定義、イベントハンドラの実装パターンを学ぶことができます。

実行結果:

ブラウザで表示すると、入力フィールドが表示されます。文字を入力すると、部分一致する候補が表示されます。

custom autocomplete React - DOWNキーでの候補選択ハイライト表示

実行結果の解説:

custom autocomplete React component が正常に動作しています。

入力フィールドに「k」と入力すると、「Tokyo」「Osaka」「Fukuoka」が候補として表示されます。これは、handleInputChange 関数内の filter メソッドと includes メソッドによる部分一致フィルタリングが機能しているためです。

toLowerCase メソッドを使用しているため、大文字小文字を区別せずにフィルタリングされます。

候補リストは、isOpen が true かつ filteredOptions.length が 0 より大きい場合にのみ表示される条件付きレンダリングになっています。

Tailwind CSS の absolute と relative による配置制御により、候補リストが入力フィールドの真下に表示されます。

これで基本的な custom autocomplete React component の実装が完了しました。

ステップ2: エラー解決 - useRef が見つからない

実装中に「useRef が見つかりません」というエラーが発生しました。

エラーメッセージ:

名前 'useRef' が見つかりません。ts(2304)

エラーの原因:

useRef をインポートしていなかったためです。React の Hooks は使用前に必ずインポートが必要です。

解決方法:

ファイルの先頭のインポート文を修正します。

// 修正前
import { useState, useEffect } from "react";

// 修正後
import { useState, useRef, useEffect } from "react";

解決の意図:

React Hooks(useState、useRef、useEffect など)は、react パッケージからインポートする必要があります。useRef は DOM 要素への参照を保持するために使用します。

解決後の結果:

エラーが解消され、TypeScript の型チェックが正常に動作するようになりました。

これで useRef を使った DOM 要素の参照管理ができるようになりました。

ステップ3: エラー解決 - listRef の型エラー

listRef の型定義で TypeScript エラーが発生しました。

エラーメッセージ:

型 'RefObject<HTMLInputElement>' の引数を型 'RefObject<HTMLUListElement> | undefined' に割り当てることはできません。

エラーの原因:

listRef の型が HTMLInputElement になっていましたが、実際には ul 要素を参照するため、型が一致していませんでした。

// 誤った型定義
const listRef = useRef<HTMLInputElement>(null);

解決方法:

listRef の型を HTMLUListElement に変更します。

// 修正前
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLInputElement>(null);

// 修正後
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);

解決の意図:

TypeScript では、各 HTML 要素に対応した型が定義されています。

  • input 要素: HTMLInputElement
  • ul 要素: HTMLUListElement
  • div 要素: HTMLDivElement
  • button 要素: HTMLButtonElement

正しい型を指定することで、TypeScript の型チェックが正常に動作し、実行時エラーを防ぐことができます。

解決後の結果:

型エラーが解消され、コンパイルが成功しました。

これで ul 要素への参照が正しく型付けされました。

ステップ4: エラー解決 - case ブロック内の変数宣言

switch 文の case ブロック内で変数を宣言すると、ESLint エラーが発生しました。

エラーメッセージ:

Unexpected lexical declaration in case block. eslint(no-case-declarations)
custom autocomplete React - switch文caseブロック内の変数宣言ESLintエラー

エラーの原因:

JavaScript の switch 文では、case ブロック内で変数を宣言する場合、スコープを明確にするために波括弧が必要です。

// エラーが出るコード
case "Enter": 
  const selectedOption = filteredOptions[selectedIndex];
  if (selectedIndex >= 0 && selectedOption) {
    handleOptionClick(selectedOption);
  }
  break;

解決方法:

case ブロックを波括弧で囲みます。

// 修正前
case "Enter": 
  const selectedOption = filteredOptions[selectedIndex];
  if (selectedIndex >= 0 && selectedOption) {
    handleOptionClick(selectedOption);
  }
  break;

// 修正後
case "Enter": {
  const selectedOption = filteredOptions[selectedIndex];
  if (selectedIndex >= 0 && selectedOption) {
    handleOptionClick(selectedOption);
  }
  break;
}

解決の意図:

波括弧を追加することで、変数のスコープが case ブロック内に限定されます。これにより、他の case ブロックとの名前衝突を防ぎ、コードの可読性も向上します。

解決後の結果:

ESLint エラーが解消され、コードが正常に動作するようになりました。

これで TypeScript と ESLint の両方のチェックをパスする custom autocomplete React component が完成しました。

ステップ5: 検索アイコンの追加

入力フィールド内に虫眼鏡アイコンを配置します。

実行する操作:

lucide-react から Search アイコンをインポートし、入力フィールド内に配置します。

// ファイル先頭に追加
import { Search } from "lucide-react";

// return文内のinput部分を修正
// 修正後
<div className="relative w-64">
  <div className="relative">
    <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
    <input 
      ref={inputRef}
      type="text"
      value={inputValue}
      onChange={handleInputChange}
      onKeyDown={handleKeyDown}
      placeholder="Search..."
      className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
    />
  </div>

操作の意図:

アイコンを入力フィールド内に配置することで、ユーザーが「検索できる」ことを直感的に理解できるようにします。lucide-react は Next.js プロジェクトで標準的に使用されるアイコンライブラリです。

最初の実装では、アイコンが入力フィールドの外に表示される問題が発生しました。

custom autocomplete React - 検索アイコンが入力欄外に表示される配置エラー

実行結果:

アイコンが入力フィールドの外側に表示されてしまいました。

問題の原因:

input とアイコンを追加の div で囲んでいなかったため、アイコンの absolute 配置の基準点が正しく設定されていませんでした。

CSS の position プロパティには重要な原則があります:

absolute(絶対配置)の要素は、最も近い relative の親を基準に配置される

最初の実装では、アイコンの absolute が外側の div を基準にしてしまい、input の領域外に配置されていました。

解決方法:

input とアイコンを新しい div で囲み、その div に relative を指定します。

構造の変更:

修正前:
<div className="relative w-64">
  <Search className="absolute ..." />
  <input ... />
</div>

修正後:
<div className="relative w-64">
  <div className="relative">
    <Search className="absolute ..." />
    <input className="w-full pl-10 ..." />
  </div>
</div>

解決の意図:

新しい relative の div を追加することで、アイコンの absolute 配置の基準点が input と同じ領域になります。

視覚的に理解すると:

<div relative w-64>(全体のコンテナ)
  └─ <div relative>(新しい基準点)
       ├─ <Search absolute>(この div を基準に配置)
       └─ <input w-full>(この div いっぱいに広がる)

input に w-full を追加することで、親 div いっぱいに広がります。pl-10 を追加することで、アイコンの分だけ左パディングを確保し、テキストがアイコンに重ならないようにします。

解決後の結果:

アイコンが入力フィールドの左側内部に正しく配置されました。

relative と absolute の関係を理解することで、要素を正確に重ねて配置できるようになりました。この原則は、ドロップダウン、ツールチップ、モーダルなど、あらゆる場面で活用できます。

ステップ6: 動作確認

実装した custom autocomplete React component のすべての機能を確認しました。

キーボード操作の確認:

  1. DOWN キー: tokyo から osaka、fukuoka の順にハイライト
  2. UP キー: fukuoka から osaka の順にハイライト
  3. ENTER キー: 選択が確定
  4. ESC キー: 候補リストが閉じる
custom autocomplete React - DOWNキーでの候補選択ハイライト表示

マウス操作の確認:

  1. 候補をホバー: 水色背景に変化(hover:bg-blue-100)
  2. 候補をクリック: 選択が確定
  3. 外側をクリック: 候補リストが閉じる

フィルタリング機能の確認:

  1. "tokyo" と入力: "Tokyo" が表示される(大文字小文字を無視)
  2. "OSAKA" と入力: "Osaka" が表示される
  3. "k" と入力: "Tokyo"、"Osaka"、"Fukuoka" が表示される(部分一致)

動作確認の結果:

すべての機能が正常に動作しました。

実装した機能:

  • テキスト入力に応じた候補表示
  • 大文字小文字を無視したフィルタリング
  • キーボード操作(UP/DOWN/ENTER/ESC)
  • マウスクリックで選択
  • 外側クリックで閉じる
  • 選択中の候補をハイライト表示
  • 入力フィールド内にアイコン表示

これで custom autocomplete React component の実装と動作確認が完了しました。

ハンズオンの中で疑問に感じた点や失敗した点

実際に直面した疑問と失敗

実装中に以下の疑問と失敗を経験しました。

疑問1: DOM要素とは何か

custom autocomplete React を実装する中で、「DOM要素」という言葉が何度も出てきましたが、最初は意味が理解できませんでした。

疑問の内容:

DOM要素とは具体的に何を指すのか。

解決手段:

DOM は Document Object Model の略で、HTMLをプログラムで操作できるオブジェクトとして表現したものです。

具体例:

<div>
  <input type="text" />
  <button>送信</button>
</div>

これをブラウザが読み込むと、以下のようなオブジェクト構造が作られます:

Document
  └─ div要素
       ├─ input要素
       └─ button要素

このオブジェクト構造が DOM で、個々の要素(input要素、button要素など)が DOM要素です。

制御系の経験がある方には、以下のように理解するとわかりやすいです:

設計図(CAD)        → HTML
実際の製品の構造データ → DOM
各パーツ            → DOM要素

疑問2: e.target.value の意味

handleInputChange 関数内の e.target.value が何を取得しているのか疑問に感じました。

疑問の内容:

ユーザーが「hog」と1文字ずつ入力する場合、e.target.value には2文字目以降の文字が格納されているのか。

解決手段:

e.target.value は「今入力された1文字」ではなく「入力フィールドの全文字列」を取得します。

具体例:

タイミング1: 「h」を入力
e.target.value = "h"(フィールド全体の内容)

タイミング2: 「o」を入力
e.target.value = "ho"(フィールド全体の内容)

タイミング3: 「g」を入力
e.target.value = "hog"(フィールド全体の内容)

理由:

入力フィールド(input)というDOM要素は、常に「現在の値全体」を持っています。onChange イベントが発生したとき、e.target はその入力フィールド全体を指すため、e.target.value はフィールドの全内容になります。

疑問3: default export と named export の違い

Header コンポーネントをインポートする際に、波括弧の有無でエラーが発生しました。

疑問の内容:

なぜ import { Header } ではエラーになり、import Header ではエラーにならないのか。

解決手段:

export の種類によって、import の書き方が変わります。

default export の場合:

// Header.tsx
export default function Header () { ... }

// インポート(波括弧なし)
import Header from "./components/Header";

named export の場合:

// Header.tsx
export function Header () { ... }

// インポート(波括弧あり)
import { Header } from "./components/Header";

重要なポイント:

default キーワードは省略できるものではなく、どちらのエクスポート方法を使うかを決めるキーワードです。

比較表:

書き方種類インポート方法
export default function Header() {}デフォルトimport Header from "…"
export function Header() {}名前付きimport { Header } from "…"

よくある失敗

初学者が custom autocomplete React を実装する際によく陥る失敗パターンをまとめました。

失敗1: relative と absolute の関係を理解していなかった

アイコンを入力フィールド内に配置しようとした際、アイコンが欄外に表示される問題が発生しました。

失敗の内容:

input とアイコンを追加の div で囲まなかったため、アイコンの配置基準点が正しく設定されませんでした。

誤った実装:

<div className="relative w-64">
  <Search className="absolute left-3 top-1/2 ..." />
  <input className="border ..." />
</div>

エラーメッセージ/結果:

アイコンが入力フィールドの外側(左上)に表示されました。

模範例:

<div className="relative w-64">
  <div className="relative">
    <Search className="absolute left-3 top-1/2 ..." />
    <input className="w-full pl-10 ..." />
  </div>
</div>

ポイント:

absolute(絶対配置)の要素は、最も近い relative の親を基準に配置されます。

新しい relative の div を追加することで、アイコンの absolute 配置の基準点が input と同じ領域になります。input に w-full を追加して親 div いっぱいに広がるようにし、pl-10 を追加してアイコンの分だけ左パディングを確保します。

失敗2: TypeScript の型定義を間違えた

listRef の型を HTMLInputElement にしてしまい、TypeScript エラーが発生しました。

失敗の内容:

ul 要素を参照する listRef の型を、誤って HTMLInputElement と定義しました。

誤った実装:

const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLInputElement>(null);

エラーメッセージ/結果:

型 'RefObject<HTMLInputElement>' の引数を型 'RefObject<HTMLUListElement> | undefined' に割り当てることはできません。

模範例:

const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLUListElement>(null);

ポイント:

TypeScript では、各 HTML 要素に対応した型が定義されています。

  • input 要素: HTMLInputElement
  • ul 要素: HTMLUListElement
  • div 要素: HTMLDivElement
  • button 要素: HTMLButtonElement

正しい型を指定することで、TypeScript の型チェックが正常に動作し、実行時エラーを防ぐことができます。

失敗3: case ブロック内で変数宣言をした

switch 文の case ブロック内で変数を宣言すると、ESLint エラーが発生しました。

失敗の内容:

case ブロック内で変数を宣言する際、波括弧で囲まなかったため ESLint エラーが発生しました。

誤った実装:

case "Enter": 
  const selectedOption = filteredOptions[selectedIndex];
  if (selectedIndex >= 0 && selectedOption) {
    handleOptionClick(selectedOption);
  }
  break;

エラーメッセージ/結果:

Unexpected lexical declaration in case block. eslint(no-case-declarations)

模範例:

case "Enter": {
  const selectedOption = filteredOptions[selectedIndex];
  if (selectedIndex >= 0 && selectedOption) {
    handleOptionClick(selectedOption);
  }
  break;
}

ポイント:

JavaScript の switch 文では、case ブロック内で変数を宣言する場合、スコープを明確にするために波括弧が必要です。波括弧を追加することで、変数のスコープが case ブロック内に限定され、他の case ブロックとの名前衝突を防ぎます。

失敗4: イベントハンドラの書き方を間違える

onClick イベントで、関数を即座に実行してしまいました。

失敗の内容:

onClick に関数の参照ではなく、関数の実行結果を渡してしまいました。

誤った実装:

<li onClick={handleOptionClick(option)}>
  {option}
</li>

エラーメッセージ/結果:

レンダリング時に handleOptionClick が実行され、無限ループが発生しました。クリック時ではなく、コンポーネントがレンダリングされるたびに関数が実行されます。

模範例:

<li onClick={() => handleOptionClick(option)}>
  {option}
</li>

ポイント:

イベントハンドラには、関数の参照を渡す必要があります。

正しい書き方:

  • onClick={handleClick}: 引数なしの場合
  • onClick={() => handleClick(arg)}: 引数ありの場合(アロー関数で囲む)

間違った書き方:

  • onClick={handleClick()}: 即座に実行される(return値が onClick に渡される)

custom autocomplete React では、option という引数を渡す必要があるため、アロー関数を使用しています。

失敗5: Next.js で "use client" を忘れる

Next.js プロジェクトで React Hooks を使う際、"use client" ディレクティブを追加しませんでした。

失敗の内容:

ファイルの先頭に "use client" を追加しなかったため、エラーが発生しました。

誤った実装:

import { useState, useRef, useEffect } from "react";

export default function AutoComplete() {
  const [inputValue, setInputValue] = useState("");
  // ...
}

エラーメッセージ/結果:

You're importing a component that needs `useEffect`. This React Hook only works in a Client Component.

模範例:

"use client"

import { useState, useRef, useEffect } from "react";

export default function AutoComplete() {
  const [inputValue, setInputValue] = useState("");
  // ...
}

ポイント:

Next.js では、デフォルトで Server Component として動作します。React Hooks(useState、useEffect、useRef など)を使用する場合は、ファイルの先頭に "use client" ディレクティブを追加して、Client Component として明示する必要があります。

Client Component が必要な場合:

  • React Hooks を使用
  • ブラウザ API を使用(document、window など)
  • イベントハンドラを使用(onClick、onChange など)

custom autocomplete React は、すべて Client Component で動作する機能のため、"use client" が必須です。

よくある疑問

初学者が custom autocomplete React を実装する際によく抱く疑問を Q&A 形式でまとめました。

疑問1: なぜ useRef を使うのか

useState と useRef はどちらも値を保持できますが、なぜ DOM要素の参照には useRef を使うのですか。

解決手段:

useState と useRef の使い分けは以下の通りです。

useState:

  • 値が変わると再レンダリングが発生
  • UI に表示する値の管理に使用
  • 例: 入力フィールドの値、候補リストの表示状態

useRef:

  • 値が変わっても再レンダリングが発生しない
  • DOM要素への参照や、レンダリングに影響しない値の保持に使用
  • 例: input要素への参照、タイマーIDの保持

custom autocomplete React では、DOM要素への参照(外側クリック検知のため)に useRef を使用しています。

疑問2: e.preventDefault() は何のために使うのか

キーボードイベントで e.preventDefault() を呼んでいますが、これは何をしているのですか。

解決手段:

e.preventDefault() は、ブラウザのデフォルト動作を無効化するメソッドです。

具体例:

ArrowDown キーを押した場合:

e.preventDefault() がない場合:

  • 候補の選択位置が変わる(期待通り)
  • ブラウザのデフォルト動作で、カーソルが入力フィールドの末尾に移動してしまう

e.preventDefault() がある場合:

  • 候補の選択位置が変わる(期待通り)
  • ブラウザのデフォルト動作は発生しない

custom autocomplete React では、キーボード操作を候補選択にだけ使いたいため、カーソル移動などのデフォルト動作を無効化しています。

疑問3: filter と includes の組み合わせは何をしているのか

フィルタリング処理で filter と includes を組み合わせていますが、どういう動作をしているのですか。

解決手段:

filter メソッドは配列から条件に合う要素だけを抽出し、includes メソッドは文字列に特定の文字が含まれるかをチェックします。

具体例:

const filtered = cities.filter((city) => 
  city.toLowerCase().includes(value.toLowerCase())
);

動作の流れ:

  1. cities 配列の各要素(city)に対して処理を実行
  2. city.toLowerCase() で小文字に変換
  3. value.toLowerCase() で入力値も小文字に変換
  4. includes で部分一致チェック
  5. true を返した要素だけが filtered 配列に入る

入力値が "k" の場合:

"tokyo".includes("k") → true
"osaka".includes("k") → true
"nagoya".includes("k") → false
"sapporo".includes("k") → false
"fukuoka".includes("k") → true

結果: ["Tokyo", "Osaka", "Fukuoka"]

大文字小文字を toLowerCase() で統一することで、"TOKYO"、"tokyo"、"ToKyO" のどの入力でも "Tokyo" にマッチします。

疑問4: なぜ条件付きレンダリングを使うのか

候補リストの表示に isOpen && filteredOptions.length という条件を使っていますが、なぜ両方の条件が必要なのですか。

解決手段:

2つの条件を組み合わせることで、適切なタイミングでのみ候補リストを表示できます。

条件の意味:

isOpen: 候補リストを表示する状態かどうか
filteredOptions.length > 0: 表示する候補が実際に存在するか

具体例:

ケース1: 入力が空

  • isOpen: false
  • filteredOptions.length: 0
  • 結果: 候補リスト非表示(正しい)

ケース2: 入力があるがマッチしない

  • isOpen: true
  • filteredOptions.length: 0
  • 結果: 候補リスト非表示(正しい。空のリストを表示しない)

ケース3: 入力があってマッチする

  • isOpen: true
  • filteredOptions.length: 3
  • 結果: 候補リスト表示(正しい)

両方の条件をチェックすることで、候補が存在する場合のみリストを表示し、ユーザー体験を向上させます。

今回のまとめ

AI assistant Claude との対話を通じて、custom autocomplete React component の実装方法を学習しました。お疲れ様でした。

今回学習したこと:

  • custom autocomplete React を TypeScript で一から実装する方法と、React Hooks(useState、useRef、useEffect)の実践的な使い方
  • relative と absolute による CSS 配置制御の原則と、DOM要素の参照管理の仕組み
  • TypeScript の型定義エラーの解決方法と、Next.js における Client Component の使い分け

この記事が、custom autocomplete React の実装を学ぶきっかけになれば幸いです。また別の記事でお会いしましょう。

-dev, TailwindCSS