[Remix] Remixでインクリメンタルサーチを実装する

2024.05.02

情報システム室の進地@日比谷です。

Remixでインクリメンタルサーチを実装する方法について記述します。

会社のインクリメンタルサーチの動作イメージ

仕様

Homeページに設置した検索ボックスでインクリメンタルサーチを実装します。会社の情報が入ったDBがあるとして、会社名で候補検索する仕様を想定します。

ファイル構成

Remixの標準構成に app/model/company.tsapp/routes/companies.tsx を追加しています。 前者はCompanyデータ形と会社データのサンプルを記載し、後者には会社データを取得するloaderを記載しています。

app
├── entry.client.tsx
├── entry.server.tsx
├── models
│   └── company.ts
├── root.tsx
└── routes
    ├── _index.tsx
    └── companies.tsx

コード

idとnameを文字列としてもつCompany型を定義しています。また、getCompanies()メソッドでダミーの会社情報を返すようにしています。

ここを外部DBなどから取得するように変更すれば当該DBに対してインクリメンタルサーチができるようになります。

app/models/company.ts

export type Company = {
  id: string;
  name: string;
};

export async function getCompanies(): Promise<Array<Company>> {
  return [
    {
      id: "aaa",
      name: "ウエキン合同会社",
    },
    {
      id: "bbb",
      name: "ウエキンコーポレーション",
    },
    {
      id: "ccc",
      name: "シンチ有限会社",
    },
  ];
}

loaderを定義します。GETパラメータのtermから検索文字列を受け取り、先述のgetCompanies()で取得したデータに対してフィルタして返しています。

app/routes/companies.tsx

import { json, LoaderFunctionArgs } from "@remix-run/node";

import { Company, getCompanies } from "~/models/company";

export const loader = async ({
  request,
}: LoaderFunctionArgs) => {
  const url = new URL(request.url);
  const term = url.searchParams.get('term');
  return json(await searchCompany('' + term));
}

async function searchCompany(term: string): Promise<Array<Company>> {
  const companies = await getCompanies();
  return companies.filter(company => company.name?.match(term));
}

最後に検索窓を置くHome画面(Index)の実装です。

fetcherを使って、/companies?term=検索ワードの呼び出しを行い、結果をfetcher.dataで受け取ってレンダリングします。

<input type="search"><datalist>を組み合わせることで簡単に候補表示が可能です。

app/routes/_index.tsx

import type { MetaFunction } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";

import { Company } from "~/models/company";

export const meta: MetaFunction = () => {
  return [
    { title: "Home" },
    { name: "description", content: "This is your portal site!" },
  ];
};

export default function Index() {
  const fetcher = useFetcher();

  return (
    <div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.8" }}>
      <fetcher.Form>
        <input
          type="search"
          onChange={(e) => {
            const term = e.currentTarget.value;
            fetcher.load(`/companies?term=${term}`);
          }}
          list="suggestions"
        />
        <datalist id="suggestions">
          {fetcher.data?.map((company: Company) => {
            return (
              <option key={company.id} value={company.name} />
            )
          })}
        </datalist>
      </fetcher.Form>
    </div>
  );
}