【React】予測候補が複数ある郵便番号からの住所入力補完コンポーネントの作成


フロントエンドエンジニアの茶木です。
前回の記事で zipcloud という 郵便番号APIと React を組み合わせて、予測候補の住所を入力をする記事を書きました。今回は改良版を作ります。

前回の課題の確認

前回作成したコンポーネントは、ユーザーの郵便番号入力に対応する住所があれば、住所を自動的にフィルするものでした。この例では、ユーザーが郵便番号に 120-0000と入力し、都道府県・市区町村に 東京都 足立区 が表示されています。

課題は以下の2点でした。

  • 先に住所を入力してから、郵便番号を入力すると住所が予測で上書きされる
  • 住所の候補が複数あるケースがあり、先頭以外を握り潰している

予測で住所が上書きされる

ユーザーが都道府県・市区町村を先に入力し、郵便番号を入力すると上書きされます。通常、ユーザー入力の都道府県・市区町村の内容は、郵便番号からの補完と一致するはずなので上書きされても問題ないのですが、ユーザーの手入力が可能なのは、郵便番号と一致しない例外を手入力で対応するためでもあります。

住所の複数候補、先頭以外を握り潰している

実は、郵便番号は住所の町名と一対一に対応していません。たとえば、北海道 美唄市にある、上美唄町協和、上美唄町南、上美唄町 の3つの町は郵便番号 079-0177 が割り振られています。そのため zipcloud の APIの検索結果も3つ返却されています。

こういったケースでは先頭の上美唄町協和をフィルしています。
(ユーザーにとって目的の町名でない場合は手入力で修正するのを想定)

{
	"message": null,
	"results": [
		{
			"address1": "北海道", "address2": "美唄市", "address3": "上美唄町協和",
			"kana1": "ホッカイドウ", "kana2": "ビバイシ", "kana3": "カミビバイチョウキョウワ",
			"prefcode": "1",
			"zipcode": "0790177"
		},
		{
			"address1": "北海道", "address2": "美唄市", "address3": "上美唄町南",
			"kana1": "ホッカイドウ", "kana2": "ビバイシ", "kana3": "カミビバイチョウミナミ",
			"prefcode": "1",
			"zipcode": "0790177"
		},
		{
			"address1": "北海道", "address2": "美唄市", "address3": "上美唄町",
			"kana1": "ホッカイドウ", "kana2": "ビバイシ", "kana3": "カミビバイチョウ",
			"prefcode": "1",
			"zipcode": "0790177"
		}
	],
	"status": 200
}

https://zipcloud.ibsnet.co.jp/api/search?zipcode=0790177

解決プラン

郵便番号入力後の予測住所をツールチップで表示し、ユーザーのクリックで確定しフィルします。複数候補がある場合は、すべての候補をツールチップで表示します。ユーザーは、明示的にツールチップの補完候補から選択でき、また補完を無視しても良いです。

実装

getCandidateAddresses

import { getAddress } from "../pages/api/zipcode";

export const getCandidateAddresses = async (
  zipcode: string
): Promise<string[]> => {
  const res = await getAddress(zipcode);
  return res.data.results.map((item) => `${item.address1} ${item.address2} ${item.address3}`);
};

zipcloud の APIから使う情報だけ取り出すようにしたメソッドです。 getAddress が zipcloud API のラップ部分です。配列ですべて address1, address2, addres3 を繋げてすべての候補を返します。

ZipCodeInput

interface Props {
  zipcode: string;
  onChange: (code: string) => void;
  onSelectAddressBody: (addressBody: string) => void;
}

export function ZipCodeInput({
  zipcode,
  onChange,
  onSelectAddressBody,
}: Props) {
  const [tooltipContent, setTooltipContent] = useState<ReactNode>(undefined);
  const [focus, setFocus] = useState(false);
  const valid = isValidZipcode(zipcode)
  useEffect(() => {
    if (!valid) {
      setTooltipContent(undefined);
      return;
    }
    getCandidateAddresses(zipcode).then((candidates) => {
      if (candidates.length) {
        setTooltipContent(
          <List>
            {candidates.map((c) => (
              <ListItem key={c}>
                <ListItemButton onClick={() => onSelectAddressBody(c)}>{c}</ListItemButton>
              </ListItem>
            ))}
          </List>
        );
        return;
      }
      setTooltipContent(
        <List>
          <ListItem>存在しない郵便番号のようです</ListItem>
        </List>
      );
    });
  }, [onSelectAddressBody, valid, zipcode]);

  return (
    <div onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}>
      <Tooltip placement="right" open={valid && focus} title={tooltipContent}>
        <TextField
          label="郵便番号"
          value={zipcode}
          onChange={(e) => onChange(e.target.value)}
          error={!valid}
          helperText={
            valid ? "" : "7桁の正しい郵便番号の形式で入力してください"
          }
        />
        <TextField
          label="都道府県・市区町村"
          value={addressBody}
          onChange={(e) => setAddressBody(e.target.value)}
        />
      </Tooltip>
    </div>
  );
}

Tooltip, TextField などは MUI を利用しています。

郵便番号入力のインプットフォームと、予測候補を表示するツールチップです。
isValidZipcode で有効な郵便番号フォーマットかを確認し、有効な場合は getCandidateAddresses をコールし、その結果で入力候補を表示しています。

呼び出し部

export default function Index(): ReactElement | null {
  const [zipcode, setZipcode] = useState("");
  const [addressBody, setAddressBody] = useState("");
  return (
    <PageBase title="予測候補が複数ある郵便番号からの住所入力補完">
      <ZipCodeInput
        zipcode={zipcode}
        onChange={setZipcode}
        onSelectAddressBody={setAddressBody}
      />
      <TextField
        label="都道府県・市区町村"
        value={addressBody}
        onChange={(e) => setAddressBody(e.target.value)}
      />
    </PageBase>
  );
}

住所は controlled input で作ればOKです。 郵便番号入力の ZipCodeInput には、住所の onChange に渡すのと同じく setAddressBody を渡しています。(番地・建物名・部屋番号は予測変換に関係しないフォームなのでコード上は省略しています)

これで、複数の予測候補がある場合でもユーザーが任意で選択できますし、先に住所入力を済ませたようなケースなどでは候補の無視もできます。

おわりに

郵便番号と住所の関係は複雑なので、UIも複雑なものになりがちです。今回、コンポーネントを考えることで気がついたことがあり、それはフォーカスのコントロールや、Tabやエンターキーでの選択や決定、入力後のフォーマットなどには改良の余地がありそうだということです。

Gaji-Labo は Next.js, React, TypeScript 開発の実績と知見があります

フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。

「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」

フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。

オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。

Next.js, React, TypeScript の相談をする!

投稿者 Chaki Hironori

webライターもやってるフロントエンドエンジニアです。Reactは自信があります。またデザイン畑の出身で、気持ちのいいアニメーションやインタラクティブな表現は丁寧に手掛けます。好きなものは中南米の遺跡で、スペイン語が少しできます。