debounceを使って、入力後にFetch処理(Material UI の Autocomplete で複数選択を Fetch で候補検索しながら選択していくサンプル)


初めに

タイトルが説明的でかつ長い、、
debounceを使ったAutocompleteがあまりにもひどかったので、きちんとそのあたりも含めての見直し記事
前回アップしているのは削除済み

こんな感じのもの


※画面側の動作確認のために、サーバー側はFastAPI側でGoogleに候補検索をかけにいっています

コード

App.js
import { useState, useEffect } from "react";
import { TextField, Autocomplete, Stack } from "@mui/material";
import { debounce } from "lodash";
import axios from "axios";

const App = () => {
  // 前回のキーワード
  const [beforeString, setBeforeString] = useState("");
  // 表示候補(本来はreduxのuseSelectorなんかでデータを管理する)
  const [resultOptions, setResultOptions] = useState([]);
  // 選択されている内容
  const [autocompleteValues, setAutocompleteValues] = useState([]);
  // リクエストするURL
  const baseUrl = "http://localhost:8000/api/search/candidate/words/";
  // 変更が止まった後、指定時間(ミリ秒)後処理を行う
  const term = 500;
  const debounceSearchHandler = debounce(
    (changeValue) => searchMethod(changeValue),
    term
  );

  // 検索処理
  const searchMethod = async (changeValue) => {
    setBeforeString(changeValue);
    // 内容が同じ場合は処理しない
    if (changeValue.length > 0 && beforeString !== changeValue) {
      // FetchしてAutocompleteのoptionsの内容を取得する処理
      // [{title: keyword1}, {title: keyword2}, ...] が返ってくることを想定
      const responce = await axios.get(`${baseUrl}${changeValue}`);
      setResultOptions([...responce.data]);
    } else {
      // オートコンプリートの内容初期化
      setResultOptions([]);
    }
  };

  // 選択データが変わったときの処理
  const selectedChange = (event, selectedValue) => {
    // 選択されているオートコンプリート内容変更
    setAutocompleteValues([...selectedValue]);
    // オートコンプリートの内容初期化
    setResultOptions([]);
  };

  // 選択データの内容確認
  useEffect(() => {
    console.log("autocompleteValues", autocompleteValues);
  }, [autocompleteValues]);

  return (
    <Stack sx={{ width: 500 }}>
      <Autocomplete
        multiple // 複数選択
        freeSolo // 開閉をなくす
        options={resultOptions}
        onInputChange={(e, changeValue) => {
          debounceSearchHandler(changeValue);
        }} // テキスト内容変更時
        onChange={selectedChange} // 選択状態変更時
        renderInput={(params) => (
          <TextField
            {...params}
            placeholder="候補入力"
            InputProps={{
              ...params.InputProps,
              type: "search",
            }} // 全角入力時のオートコンプリートを出さない対策
          />
        )}
        filterOptions={(options) => options} // オートコンプリートの検索処理をなくす
        getOptionLabel={(option) => option.title}
      />
    </Stack>
  );
};

export default App;

テストのサーバー側

※FastAPIで作成

main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import json
import uvicorn
import os
from candidate import requestWords

app = FastAPI()

# クロス設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/api/search/candidate/words/{input}")
def read_item(input: str):
    keyWords = requestWords.getCandidateWords(input)
    keyMap = []
    for word in keyWords:
        addKeywordMap = {"title": word}
        keyMap.append(addKeywordMap)

    return keyMap


if __name__ == "__main__":
    uvicorn.run(app)

candidate/requestWords.py
import pprint
import requests
from lxml import etree
import numpy as np
import pandas as pd


def getCandidateWords(specWord: str):

    print(specWord)

    # 単語格納用
    google_suglist = []

    # オートコンプリートのデータを取得する
    google_r = requests.get(
        "https://www.google.com/complete/search",
        params={
            "q": specWord,
            "hl": "ja",
            "ie": "utf_8",
            "oe": "utf_8",
            "output": "toolbar",
        },
    )

    google_root = etree.XML(google_r.text)

    google_sugs = google_root.xpath("//suggestion")
    google_sugstrs = [s.get("data") for s in google_sugs]

    for ss in google_sugstrs:
        google_suglist.append(ss)

    return google_suglist

終わりに

とりあえず、動作確認大事