【React】郵便番号による予測機能のある住所入力コンポーネントの作成とその課題


フロントエンドエンジニアの茶木です。

郵便番号から住所入力予測ありの住所入力コンポーネントを作成します。 zipcloud という 郵便番号APIと React を組み合わせて作りました。

zipcloudとは

http://zipcloud.ibsnet.co.jp/

日本郵便のWebサイトで公開されている郵便番号データを再配信するサービスです。

zipcloud

今回使うのはAPIですが、加工済みのCSVの提供、更新の通知も行っています。

APIのラップ

import axios from "axios";

interface ResponseAddress {
  address1: string;
  address2: string;
  address3: string;
  zipcode: string;
}

interface Response {
  results: ResponseAddress[] | null;
  status: number;
}

const URL = "https://zipcloud.ibsnet.co.jp/api/search";

export const getAddress = async (zipcode: string) =>
  await axios.get<Response>(URL, { params: { zipcode } });

zipcode は半角数字7桁 1234567 かハイフン区切りの半角数字 123-4567 の形式のどちらかを受け付けます。

存在する郵便番号であれば、address1 に都道府県、 address2, address3 に市区町村、zipcode は郵便番号(ハイフン無し7桁半角数字)が取得できるので、 ResponseAddress の型に記載しています。

補足: API取得結果の補足

ここでは使用しないため ResponseAddress に含めていませんが、都道府県コードの prefcode もAPIの結果にあります。都道府県はコードで管理した方が良いケースの場合は、address1 の代わりに使うと良いでしょう。

それ以外にもヨミガナの kana1, kana2, kana3 もAPI取得結果に含まれます。

APIコールの前処理と後処理

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

const pattern = /^\d{3}-\d{4}$|^\d{7}$/;

function isValidZipcode(input: string): boolean {
  return pattern.test(toHalfWidth(input));
}

function toHalfWidth(str: string): string {
  return str.replace(/[0-9]/g, (s) =>
    String.fromCharCode(s.charCodeAt(0) - 0xfee0)
  );
}

interface ResponseAddress {
  error: false;
  addressBody: string;
  zipcode: string;
}
interface ErrorAddress {
  error: true;
  message: string;
}

export const getAddressFromZipcode = async (
  zipcode: string
): Promise<ResponseAddress | ErrorAddress> => {
  const fixed = toHalfWidth(zipcode);
  if (!isValidZipcode(fixed)) {
    return { error: true, message: "7桁の郵便番号を入力してください" };
  }
  const res = await getAddress(fixed);
  if (!res.data.results) {
    return { error: true, message: "存在しない郵便番号のようです" };
  }
  return {
    error: false,
    addressBody: `${res.data.results[0].address1} ${res.data.results[0].address2} ${res.data.results[0].address3}`,
    zipcode: res.data.results[0].zipcode,
  };
};

getAddressFromZipcode の中で前処理・後処理を行います。

前処理

APIコール前に、郵便番号形式を満たさないものを弾いて無駄なコールをせず、すぐにエラーメッセージを返すようにします。また、全角数字から半角数字への変換も行います。

後処理

郵便番号の形式は正しいが、存在しない郵便番号があります。これらはAPIコール後にエラーとして判明するので、コール後にエラーメッセージを返すようにします。

正常に取得が完了した場合 address1, address2, address3 を繋げて住所 addressBody を作成します。実は取得結果の results は配列です。これは郵便番号が複数の市区町村を指すケースがあるためです。ユーザーが期待する市区町村は判断がつかないため、ここでは一律で配列の先頭を使用します。

住所入力コンポーネントとして組み立てる

export default function Index(): ReactElement | null {
  const [zipcode, setZipcode] = useState("");
  const [addressBody, setAddressBody] = useState("");
  const [suffix, setSuffix] = useState("");
  const [zipcodeErrorMessage, setZipcodeErrorMessage] = useState("");

  useEffect(() => {
    getAddressFromZipcode(zipcode).then((res) => {
      if (!zipcode) return setZipcodeErrorMessage("");
      if (res.error) return setZipcodeErrorMessage(res.message);
      setZipcode(res.zipcode);
      setAddressBody(res.addressBody);
      setZipcodeErrorMessage("");
    });
  }, [zipcode]);

  return (
    <PageBase title={"AddressFromZipcode"}>
      <div>
        <h1 className={styles.title}>Address</h1>
        <TextField
          label="郵便番号"
          value={zipcode}
          onChange={(e) => setZipcode(e.target.value)}
          error={Boolean(zipcodeErrorMessage)}
          helperText={zipcodeErrorMessage}
        />
        <TextField
          label="都道府県・市区町村"
          value={addressBody}
          onChange={(e) => setAddressBody(e.target.value)}
        />
        <TextField
          label="番地・建物名・部屋番号"
          value={suffix}
          onChange={(e) => setSuffix(e.target.value)}
        />
        <Button
          variant="contained"
          color="primary"
          disabled={Boolean(
            !zipcode || !addressBody || !suffix || zipcodeErrorMessage
          )}
          onChange={submit}
        >
          登録
        </Button>
      </div>
    </PageBase>
  );
}

addressBody は郵便番号入力からも取得できる範囲の、都道府県および市区町村です。手入力も可能です。suffix は郵便番号で特定されない、番地や建物名や部屋番号などの部分です。

zipcode は郵便番号で、郵便番号形式として正しいときAPIコールをし 結果が正常であれば addressBody を書き換えます。エラーは zipcodeErrorMessage に出力されます。これらは、 useEffect を使って zipcode の変化を見張り実行しています。

登録ボタンの disabled には、未入力とエラーメッセージのバリデーションチェックをしています。

課題

いくつかの課題があります。

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

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

まず、完全に悪い対応とは言えなさそうです。ユーザーの住所の直接入力ができるので予測が間違っていても手動修正できるという考え方もあります。

ふまえて、複数候補からユーザーが選択するのもいいかもしれないです。たとえば、セレクトのポップアップが表示されユーザーが選択するような形式が考えられます。

先に住所を入力してから、郵便番号を入力すると住所が予測で上書きされ消える

これも気になる点です。

代替案はいくつかありそうです

  1. 手動で住所が入力された場合は、郵便番号の予測をしない
  2. 郵便番号を完了しないと、住所入力が enable にならない
  3. 住所の予測入力ボタンを配置して、ユーザーが明示的に押したときに上書きする
  4. 郵便番号を入れると予測のポップアップが表示されユーザーが選択する

1. は実装上の難しさがあります。ユーザー入力なのか、予測の住所が表示されているのか、予測の住所をユーザーが編集したのか判断しないといけないためです。

2. は郵便番号の入力が必須になってしまう点と、ユーザーに入力順を強制する点が良くないです。

3. はときどき見かける実装です。ボタンを押す手間がありますが、ユーザーの入力を確認しているので堅牢だと言えそうです。

4. はユーザーの判断で予測住所を決定できる点と、前項の複数候補の対応も同時に解消できそうなので、これがベストな気配がします。

おわりに

次回以降で、課題をふまえて複数予測を解決した郵便番号のコンポーネント作成に挑戦してみようと思います。

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

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

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

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

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

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

投稿者 Chaki Hironori

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