🦊

Reactで金融機関検索APIをたたいてみた

2023/11/16に公開2

はじめに

今回は、JavaScript・Reactの学習として、非同期で金融機関検索APIをたたいて、useState・useEffectなどを使用して、銀行名入力フォームを実装してみました。
記事の内容としては、コードの内容を解説するものとなっていますので、もし間違っている内容があればご指摘いただけると幸いです。
(UI部分のみの実装になります。)

概要

金融機関検索APIの「銀行くん」を使用して、銀行振込フォームを実装します。
今回の実装内容は以下になります。

  • 銀行名を入力するフォームで、1文字ずつapiを叩いて、一致する銀行名の候補をリスト形式で表示します。
  • サジェストから銀行名を選択することができる。
  • 支店名を入力すると、支店コードが自動で入力される。

https://bank.teraren.com/

使用技術

  • React (18.2.0)
  • 「銀行くん」金融機関検索API (0.1.6)

デモ

codesandboxを使用して、デモを実装してみました。
Image from Gyazo
https://r2tzyz-3000.csb.app/
https://codesandbox.io/p/github/ippei-shimizu/bank-name-search-form-codesandbox/main?embed=1&file=%2Fsrc%2Findex.js
githubはこちら

コード

import { useState, useEffect } from "react";
import "./styles.css";

export default function App() {
  const [bankName, setBankName] = useState("");
  const [suggestedBanks, setSuggestedBanks] = useState([]);
  const [isFocused, setIsFocused] = useState(false);
  const [isSuggestedOpen, setIsSuggestedOpen] = useState(true);
  const [bankCode, setBankCode] = useState("");
  const [branchCode, setBranchCode] = useState("");
  const [branchName, setBranchName] = useState("");
  const [allBanks, setAllBanks] = useState([]);

  // 銀行情報を取得
  useEffect(() => {
    const fetchAllBanks = async () => {
      let fetchedBanks = [];
      let currentPage = 1;
      let hasMore = true;

      while (currentPage <= 24) {
        const api = `https://bank.teraren.com/banks.json?page=${currentPage}`;
        const response = await fetch(api);
        const data = await response.json();

        if (data && data.length > 0) {
          fetchedBanks = fetchedBanks.concat(data);
          currentPage += 1;
        } else {
          break;
        }
      }
      setAllBanks(fetchedBanks);
    };
    fetchAllBanks();
  }, []);

  // 銀行名検索で、入力値に一致するデータをサジェストで表示させる
  const handleBankNameInputChange = async (e) => {
    setIsSuggestedOpen(true);
    const value = e.target.value;
    setBankName(value);

    if (value) {
      const filteredData = allBanks.filter(
        (bank) =>
          bank.name.includes(value) ||
          bank.hira.includes(value) ||
          bank.kana.includes(value),
      );
      setSuggestedBanks(filteredData);
    } else {
      setSuggestedBanks([]);
    }
  };

  // focusを判定
  const handleFocus = () => {
    setIsFocused(true);
  };
  const handleBlur = () => {
    setIsFocused(false);
  };

  const bankNameMessage = () => {
    if (!isFocused && bankName.length === 0) return null;
    if (bankName.length === 0 || suggestedBanks.length === 0) {
      return <li className="error-text">該当の銀行は見つかりません</li>;
    }
    return suggestedBanks.map((bank) => (
      <li
        key={bank.code}
        className="bank-name"
        onClick={() => handleBankNameClick(bank.normalize.name, bank.code)}
      >
        {bank.normalize.name}
      </li>
    ));
  };

  // サジェストされた銀行名を選択
  const handleBankNameClick = (name, code) => {
    setBankName(name);
    setBankCode(code);
    setSuggestedBanks([]);
    setIsSuggestedOpen(false);
  };

  // 選択した金融機関コードを基に全支店情報を取得
  const fetchAllBranches = async (bankCode) => {
    let allBranches = [];
    let currentPage = 1;
    let hasMore = true;

    while (hasMore) {
      const api = `https://bank.teraren.com/banks/${bankCode}/branches.json?page=${currentPage}`;
      const response = await fetch(api);
      const data = await response.json();

      if (data && data.length > 0) {
        allBranches = allBranches.concat(data);
        currentPage++;
      } else {
        hasMore = false;
      }
    }
    return allBranches;
  };

  // 支店名入力
  const handleBranchNameChange = async (e) => {
    const value = e.target.value;
    setBranchName(value);

    if (bankCode.length === 4) {
      const branches = await fetchAllBranches(bankCode);
      const branch = branches.find((branch) => branch.name.includes(value));
      if (branch) {
        setBranchCode(branch.code);
      } else {
        setBranchCode("");
      }
    }
  };

  // 支店コード入力
  const handleBranchCodeChange = async (e) => {
    const value = e.target.value;
    setBranchCode(value);

    if (bankCode.length === 4 && value.length === 3) {
      fetch(`https://bank.teraren.com/banks/${bankCode}/branches/${value}.json`)
        .then((response) => response.json())
        .then((json) => {
          setBranchName(json.name);
        })
        .catch((error) => {
          setBranchName("");
        });
    }
  };

  return (
    <>
      <form className="form">
        <div>
          <label htmlFor="searchBank">銀行名</label>
          <input
            id="searchBank"
            className="input-form"
            placeholder="銀行名を検索する"
            value={bankName}
            onChange={handleBankNameInputChange}
            onFocus={handleFocus}
            onBlur={handleBlur}
          ></input>
          {isSuggestedOpen ? (
            <ul className="bank-name-suggested">{bankNameMessage()}</ul>
          ) : (
            ""
          )}
        </div>
        <div className="form-box-code">
          <label htmlFor="BankCode">金融機関コード</label>
          <input
            id="BankCode"
            className="input-form"
            placeholder="例)0001"
            value={bankCode}
          ></input>
        </div>
        <div className="form-box-branch">
          <label htmlFor="inputBranchName">支店名</label>
          <input
            id="inputBranchName"
            className="input-form"
            placeholder="例)丸の内中央"
            type="text"
            value={branchName}
            onChange={handleBranchNameChange}
          ></input>
        </div>
        <div className="form-box">
          <label htmlFor="inputBranchCode">支店コード</label>
          <input
            id="inputBranchCode"
            className="input-form"
            placeholder="例)000"
            type="text"
            value={branchCode}
            onChange={handleBranchCodeChange}
          ></input>
        </div>
      </form>
    </>
  );
}

コード解説

useStateとuseEffectとは

まず、今回の実装内で使用したuseStateuseEffectについて簡単にまとめてみます。

useState

useStateはコンポーネントの状態を管理するためのフックになります。
今回のコードの、const [bankName, setBankName] = useState("");を例にすると、、、

  • bankName→ 現在の状態を表し、id="searchBank"のinput要素のvalueに入る値を想定しています。
  • setBankNamebankNameの状態を更新するために使用されます。handleBankNameClick内でsetBankName(name);とすることで、name(サジェストで選択した銀行名)をbankNameに渡して、状態を更新します。そうすることで、id="searchBank"のinput要素のvalueに、サジェストから選択した銀行名が挿入されます。
  • useState("");()には初期値が入ります。今回は初期値にはなにも設定していません。

useEffect

useEffectは、コンポーネントがレンダリングされた後に、コンポーネントのレンダリングとは別に処理を実行するために使用します。今回のコードでは、コンポーネントが初めて描写された(マウントされた)タイミングでfetchAllBanksが実行されて、APIからデータを取得し、銀行情報を全て取得しています。
また、依存配列([])が空のため、コンポーネントが初めて描写された時に一度だけ実行されて、全ての銀行情報を保存しています。

マウントとは

コンポーネントが初めてUIに描写されるプロセスのことをさします。

依存配列とは

この配列は、useEffectなどのフックで指定することができ、フックがコンポーネントのレンダリングプロセスとは独立して実行されるタイミングを設定することができます。

  • []空の依存配列
    • コンポーネントがマウントされた後に一度だけ実行されます。これは、APIからのデータを取得を最初に一度だけ行いたい時などに使用されます。
  • 変数を含む依存配列
    • 設定した変数が変更されたタイミングで実行されます。これは、propsの変更などに応じて処理を実行したい場合などに使用されます。

では、その他の実装についても解説してみたいと思います。

銀行名・金融機関コード入力フィールドについて

以下の箇所になります。

<div>
  <label htmlFor="searchBank">銀行名</label>
  <input
  id="searchBank"
  className="input-form"
  placeholder="銀行名を検索する"
  value={bankName}
  onChange={handleBankNameInputChange}
  onFocus={handleFocus}
  onBlur={handleBlur}
  ></input>
  {isSuggestedOpen ? (
  <ul className="bank-name-suggested">{bankNameMessage()}</ul>
  ) : (
  ""
  )}
</div>
<div className="form-box-code">
  <label htmlFor="BankCode">金融機関コード</label>
  <input
    id="BankCode"
    className="input-form"
    placeholder="例)0001"
    value={bankCode}
  ></input>
</div>

この入力フィールドでは、ユーザーが入力したテキストをもとにして、動的にサジェストとして銀行名を表示し、サジェストから銀行名を選択することができるようになっています。
また、銀行名を選択すると、該当の金融機関コードも自動的に入力されます。
【使用されている状態管理】

const [bankName, setBankName] = useState("");  // 現在、入力されている銀行名
const [suggestedBanks, setSuggestedBanks] = useState([]);  // ユーザーの入力に基づいてフィルだリングされた銀行名のリスト
const [isFocused, setIsFocused] = useState(false);  // 入力フィールドがフォーカスされているかどうか
const [isSuggestedOpen, setIsSuggestedOpen] = useState(true); // サジェストにリストが表示されるかどうか
const [bankCode, setBankCode] = useState("");
const [allBanks, setAllBanks] = useState([]);  // APIから取得した全ての銀行情報
  1. ユーザーが入力フィールドに銀行名を入力すると、handleBankNameInputChange関数が呼び出されて、フィリタリングされた銀行名がサジェストリストに表示されます。
    handleBankNameInputChange関数で行われていること】
    • setIsSuggestedOpen(true);
      isSuggestedOpentrueになるため、ユーザーが入力し始めたタイミングで、サジェストリストが表示されます。
    • const value = e.target.value;setBankName(value);
      ユーザーの入力値を取得し、bankNameの状態を更新します。これは、id="searchBank"の入力フィールドのvalueと同期しています。
    • if (value) ~
      ユーザーが入力をしている場合に、以下の処理が実行されます。
      • APIから取得した全ての銀行情報allBanksに対して、フィルタリングを行います。このフィルタリングは、APIのname(銀行名),hira(ぎんこうめい),kana(ギンコウメイ)がユーザーの入力値に含まれているかをフィルタリングしています。
      • フィルタリングにより取得した銀行名は、setSuggestedBanks(filteredData);によりサジェストリストに表示されます。ユーザーの入力値が空の場合は、setSuggestedBanks([]);を返します。
  2. handleFocushandleBlurで、サジェストリストの表示を管理しています。
  3. bankNameMessage関数は、ユーザーの入力に合わせて、動的にサジェストリストを生成しています。
    • if (!isFocused && bankName.length === 0) return null;は、先ほどのhandleFocushandleBlurで状態を管理していたisFocusedがfalseで、入力フィールドがフォーカスされていなくて、入力値が空の場合はサジェストを表示させないようにしています。
    • if (bankName.length === 0 || suggestedBanks.length === 0) ~
      こちらの条件分岐処理の中では、入力値が空の場合・サジェストに銀行名がない場合は、エラーメッセージを表示して、そうでない場合はmap関数を使用して、銀行名のサジェストリストを表示しています。
  4. handleBankNameClick関数は、サジェストリストから選択された銀行名と金融機関コードをそれぞれの入力フィールドに挿入する処理を行なっています。
    具体的には、setBankName(name);bankNameの状態を更新し、setBankCode(code);bankCodeの状態を更新しています。そして、setSuggestedBanks([]);でサジェストリストを空にして、setIsSuggestedOpen(false);でサジェストを非表示にしています。

支店名・支店コード入力フィールド

以下の箇所になります。

<div className="form-box-branch">
  <label htmlFor="inputBranchName">支店名</label>
  <input
    id="inputBranchName"
    className="input-form"
    placeholder="例)丸の内中央"
    type="text"
    value={branchName}
    onChange={handleBranchNameChange}
  ></input>
</div>
<div className="form-box">
  <label htmlFor="inputBranchCode">支店コード</label>
  <input
    id="inputBranchCode"
    className="input-form"
    placeholder="例)000"
    type="text"
    value={branchCode}
    onChange={handleBranchCodeChange}
  ></input>
</div>

こちらは、先ほど選択した銀行に存在する「支店名」と「支店コード」を入力する実装になります。
【使用されている状態管理】

const [branchCode, setBranchCode] = useState("");
const [branchName, setBranchName] = useState("");
  1. fetchAllBranches関数では、先ほど選択した銀行の金融機関コードに該当する全支店情報を、非同期で取得しています。
    • asyncawaitについて(今回の実装を基に解説)
      • asyncfetchAllBranches関数を非同期関数として定義することができます。非同期関数とすることで、内部での非同期処理の完了を待ってから、Promiseを返すことができます。こちらは、bankCodeに基づきAPI通信が行われています。
      • awaitawaitで定義したAPIのリクエストが完了し、レスポンスが返ってくるまで処理を待機します。
        ようは、fetchAllBranches関数が呼び出される → 最初に定義されている変数初期化 → while文の処理 → awaitでAPIのレスポンスが返ってくるまで待機 → APIリクスト完了 → response.json()でレスポンスをjsonで解析 → jsonにdataがあれば、allBranches配列に追加 → whileループ終了 → return allBranches;という流れになり、 https://bank.teraren.com/banks/${bankCode}/branches.json?page=${currentPage}というAPIの実行が完了するまで、他の処理をブロックせずにfetchAllBranches関数内の処理は待機して、APIのレスポンスが返ってきたら次の処理に進めるということになります。
  2. handleBranchNameChange関数は、ユーザーが入力した支店名に基づいて、支店コードを設定する処理を行なっています。
    • ユーザーの入力値をsetBranchName(value);によって、barnchNameの状態を更新して、入力フォームに値を挿入しています。
    • 次に、const branches = await fetchAllBranches(bankCode);で、fetchAllBranches関数を呼び出して、指定された金融機関コードbankCodeに基づいた全支店情報を取得します。ここで、awaitを使用している理由としては、fetchAllBarnches関数が非同期処理を行なっているため、その完了を待つ必要があるからになります。
    • 次に、branches.find((branch) => branch.name.includes(value));でユーザーが入力した支店名と一致する支店名を先ほど取得したリストから検索し、もし一致した支店名があれば、setBranchCode(branch.code);branchCodeの状態に設定します。

以上の流れで、支店名入力フィールドに入力された値が存在している支店だった場合、支店コード入力フィールドにも自動的に値が挿入されます。
次に、支店コード入力フィールドです。

  1. handleBranchCodeChange関数は、ユーザーが入力した支店コードを基に、支店名を設定する処理を行なっています。
    • こちらも今までと同様に、setBranchCode(value);で入力された値で、branchCodeの状態を更新します。
    • 次に、有効な金融機関コードが入力されている場合、fetch関数で金融機関コードと支店コードに対応するAPIエンドポイントからデータを取得します。そして、.thenメソッドを使用して、取得したレスポンスをjson形式で解析し、支店名を取得し、setBranchName(json.name);で、branchNameの状態に設定します。また、 .catchでエラーハンドリングを行い、エラー時にはbranchNameに空の値が入るようにしています。

最後に

今回は、フロント側のみの実装でしたが、今後はバックエンド側のアウトプットも行なっていきたいと思います。

GitHubで編集を提案

Discussion

Matsukura YukiMatsukura Yuki

https://bank.teraren.com/ の作者です。

1回で全銀行のリストを取得できるパラメータがあります!

% curl -s 'https://bank.teraren.com/banks.json?per=10000' | jq|grep code|wc -l
    1158
いっぺいいっぺい

返信が遅くなってしまい申し訳ございません!
1回で全銀行のリストを取得できるパラメータがあるの気づきませんでした😭
わざわざコメントしてくださりありがとうございます!