lelelemon’s blog

カメの歩みでのんびり学んでいます。

【React】APIコールして取得結果を画面に反映する

React を使って外部APIをコールし、取得したレスポンスを画面に反映する簡単なサンプルです。

一連のサンプルコードは こちら にプッシュしています。

 

サンプル実行キャプチャ

 

 

実行環境について

下記の環境で動かしています。

  • Ubuntu 20.04.4 LTS (Focal Fossa)
  • go version go1.21.6 linux/amd64
  • Node v16.20.2
  • yarn 1.22.21
  • React 18.2.0
  • TypeScript 4.9.5

 

外部APIについて

【Go言語】go/gin で簡単なREST API を作成 にて作成したAPI を使用しています。

 

事前準備

docker-compose.yml
version: '3'
services:
  postgres:
    image: 'postgres:14'
    container_name: postgres_gin_rest
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: sample
      TZ: Asia/Tokyo
      PGDATA: /var/lib/postgresql/data/pgdata
    ports:
      - '5432:5432'
    volumes:
      - './db-store:/var/lib/postgresql/data'

 

docker-compose up -d 

コマンドで PostgreSQL を起動した後、

docker exec -it postgres_gin_rest psql -U user -d sample

コマンドでDBに接続して、下記CREATE文でUserテーブルを作成しておきます。

CREATE TABLE "user" (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255),
    age INT
);

 

React を使って外部APIをコールサンプル実装

モデル定義
api/model/User.ts
type User = {
  Id?: number;
  Name: String;
  Age: number;
};

export default User;
  • APIレスポンスを受けるUserモデルを定義

 

APIコール処理
api/UserApi.ts
import User from "./model/User";

export const fetchUserData = async (id: number): Promise<User | null> => {
  try {
    const response = await fetch(`http://localhost:8080/user/${id}`);
    if (!response.ok) {
      console.error("Error fetching user data. Status:", response.status);
      return null;
    }

    const responseData: { user: User } = await response.json();
    const userData: User = responseData.user;
    return userData;
  } catch (error) {
    console.error("Error fetching user data:", error);
    return null;
  }
};

export const fetchUserDatas = async (): Promise<User | null> => {
  try {
    const response = await fetch("http://localhost:8080/users");
    if (!response.ok) {
      // レスポンスが成功でない場合の処理
      console.error("Error fetching user data. Status:", response.status);
      return ;
    }

    const { users }: { users: User } = await response.json();
    return users || ;
  } catch (error) {
    console.error("Error fetching user data:", error);
    return null;
  }
};

export const RegisterUser = async (userData: User): Promise<User | null> => {
  try {
    const response = await fetch("http://localhost:8080/user", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(userData),
    });

    if (!response.ok) {
      console.error("Error registering user. Status:", response.status);
      return null;
    }

    const registeredUser: User = await response.json();
    return registeredUser;
  } catch (error) {
    console.error("Error registering user:", error);
    return null;
  }
};

export const UpdateUser = async (userData: User): Promise<User | null> => {
  try {
    const apiUrl = `http://localhost:8080/user/${userData.Id}`;

    const response = await fetch(apiUrl, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(userData),
    });

    if (!response.ok) {
      console.error("Failed to update user");
      return null;
    }

    const updatedUser: User = await response.json();
    return updatedUser;
  } catch (error) {
    console.error("Error updating user:", error);
    return null;
  }
};

export const deleteUser = async (userId: number): Promise<boolean> => {
  try {
    const apiUrl = `http://localhost:8080/user/${userId}`;

    const response = await fetch(apiUrl, {
      method: "DELETE",
      headers: {
        "Content-Type": "application/json",
      },
    });

    if (!response.ok) {
      console.error("Failed to delete user");
      return false;
    }

    return true;
  } catch (error) {
    console.error("Error deleting user:", error);
    return false;
  }
};
  • 外部APICRUDに対応する処理をここに記述しています。

 

ここまででモデルとAPI呼び出し処理ができたので、画面コンポーネントを作成していきます。

 

トップ画面
component/UserComponent.tsx
import { useEffect, useState } from "react";
import User from "../api/model/User";
import { deleteUser, fetchUserDatas } from "../api/UserApi";
import "./UserComponent.css";
import { Link } from "react-router-dom";

const UserComponent: React.FC = () => {
  const [userList, setUserList] = useState<User | null>(null);
  const [isDeleteDialogOpen, setDeleteDialogOpen] = useState<boolean>(false);
  const [deletingUserId, setDeletingUserId] = useState<number | null>(null);

  useEffect(() => {//➀
    const fetchData = async () => {
      const data = await fetchUserDatas();
      setUserList(data);
    };

    fetchData();
  }, );

  const handleDeleteClick = (userId: number) => {
    setDeletingUserId(userId);
    setDeleteDialogOpen(true);
  };

  const handleDeleteConfirm = async () => {
    if (deletingUserId !== null) {
      try {
        const isDeleted = await deleteUser(deletingUserId);

        if (isDeleted) {
          setDeleteDialogOpen(false);
          const updatedData = await fetchUserDatas();
          setUserList(updatedData);
        } else {
          console.error("Error deleting user");
        }
      } catch (error) {
        console.error("Error deleting user data:", error);
      }
    }
  };

  const handleDeleteCancel = () => {
    setDeletingUserId(null);
    setDeleteDialogOpen(false);
  };

  return (
    <div>
      {userList ? (
        <div>
          <h2>User List</h2>
          <table className="user-table">
            <thead>
              <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Age</th>
              </tr>
            </thead>
            <tbody>
              {userList.map*1}
            </tbody>
          </table>
          <div>
            <Link to={"/user/register"}>新規ユーザー追加</Link>//➂
          </div>

          {/* 削除ダイアログ */}
          {isDeleteDialogOpen && (
            <div className="delete-dialog">
              <p>本当に削除しますか?</p>
              <button onClick={handleDeleteConfirm}>はい</button>
              <button onClick={handleDeleteCancel}>いいえ</button>
            </div>
          )}
        </div>
      ) : (
        <p>ユーザーデータを読み込んでいます...</p>//➁
      )}
    </div>
  );
};

export default UserComponent;
  1. 副作用フック (useEffect) を使ってコンポーネント読み込み完了後に fetchUserDatas() を呼び出し、APIからユーザーの一覧を取得します
  2. データの読み込みが完了するまでは、「ユーザーデータを読み込んでいます...」を表示しておきます
  3. 「新規ユーザー追加」および「更新」をクリックしたら、それぞれの画面に遷移します
  4. 「削除」をクリックしたら、削除画面に遷移します

 

登録画面
component/user-register/UserRegister.tsx
import { useState } from "react";
import { RegisterUser } from "../../api/UserApi";
import { Link } from "react-router-dom";

const UserRegister: React.FC = () => {
  const [name, setName] = useState<string>("");//➀
  const [age, setAge] = useState<number>(0);
  const [isSubmitSuccess, setIsSubmitSuccess] = useState<boolean>(false);

  const handleRegister = async () => {
    const registeredUserData = await RegisterUser({
      Name: name,
      Age: age,
    });

    if (registeredUserData) {
      setIsSubmitSuccess(true);
    }
  };

  return (
    <div>
      <div>
        <h2>User Register</h2>

        <div style={{ marginBottom: "10px" }}>
          <label>Name</label>
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>

        <div style={{ marginBottom: "10px" }}>
          <label>Age</label>
          <input
            type="number"
            value={age}
            onChange={(e) => setAge(parseInt(e.target.value, 10))}
          />
        </div>

        <div>
          <button onClick={handleRegister} style={{ marginRight: "5px" }}>
            登録
          </button>
          <Link to={"/"}>戻る</Link>
        </div>

        {isSubmitSuccess && <p>登録しました。</p>}
      </div>
    </div>
  );
};

export default UserRegister;
  1. useState フックで画面の入力状態を保持します

 

更新画面
component/user-update/UserUpdate.tsx
import { useEffect, useState } from "react";
import User from "../../api/model/User";
import { UpdateUser, fetchUserData } from "../../api/UserApi";
import { Link, useParams } from "react-router-dom";

const UserUpdate: React.FC = () => {
  const { id } = useParams();//➀
  const numericId: number = id ? parseInt(id, 10) : 0;

  const [user, setUser] = useState<User | null>(null);
  const [updatedName, setUpdatedName] = useState<string>("");
  const [updatedAge, setUpdatedAge] = useState<number | undefined>(undefined);
  const [isUpdateSuccess, setIsUpdateSuccess] = useState<boolean>(false);

  useEffect*2}
            />
          </div>

          <div>
            <button onClick={handleUpdate} style={{ marginRight: "5px" }}>
              Update
            </button>
            <Link to={"/"}>戻る</Link>
          </div>

          {isUpdateSuccess && <p>更新しました。</p>}
        </div>
      ) : (
        <p>Loading user data...</p>
      )}
    </div>
  );
};

export default UserUpdate;
  1. useParams フックでURLパラメーターを取得します。後で記載するルーティング設定にて、
    <Route path="user/update/:id" element={<UserUpdate />} />

    の定義を行っており、ここで指定している :id の値を読み込んでいます

 

 

ルーティング設定
App.tsx
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import UserComponent from "./component/UserComponent";
import { Route, Router, Routes } from "react-router-dom";
import UserUpdate from "./component/user-update/UserUpdate";
import UserRegister from "./component/user-register/UserRegister";

const App: React.FC = () => {
  return (
    <Routes>
      <Route path="/" element={<UserComponent />} />
      <Route path="user/register" element={<UserRegister />} />
      <Route path="user/update/:id" element={<UserUpdate />} />
    </Routes>
  );
};

export default App;

React router を使用して設定しています。パスに対してどのコンポーネントを読み込むか、シンプルにルーティングを定義できます。詳しくは下記を参照ください。

Feature Overview v6.22.0 | React Router

 

以上、サンプルになります。

 

画面UIやファイル・コンポーネント名などの構成はかなり雑です... 今回は React で外部APIをコールしてレスポンスを画面に反映するまでの簡単な流れを記載したかったので、そのほかの要素は省いています。

 

*1:user) => (

                <tr key={user.Id}>
                  <td>{user.Id}</td>
                  <td>{user.Name}</td>
                  <td>{user.Age}</td>
                  <td>
                    <Link to={`/user/update/${user.Id}`}>更新</Link>//➂
                    {" | "}
                    <button onClick={() => handleDeleteClick(user.Id!)}>
                      削除//➃
                    </button>
                  </td>
                </tr>
             

*2:) => {

    const fetchData = async () => {
      const data = await fetchUserData(numericId);
      setUser(data);
    };

    fetchData();
  }, []);

  const handleUpdate = async () => {
    if (user) {
      const updatedUserData = await UpdateUser({
        Id: numericId,
        Name: updatedName || user.Name,
        Age: updatedAge !== undefined ? updatedAge : user.Age,
      });

      if (updatedUserData) {
        setIsUpdateSuccess(true);
      }
    }
  };

  return (
    <div>
      {user ? (
        <div>
          <h2>User Update</h2>
          <p>ID: {user.Id}</p>
          <p>Name: {user.Name}</p>
          <p>Age: {user.Age}</p>

          <div style={{ marginBottom: "10px" }}>
            <label>Updated Name:</label>
            <input
              type="text"
              value={updatedName}
              onChange={(e) => setUpdatedName(e.target.value)}
            />
          </div>

          <div style={{ marginBottom: "10px" }}>
            <label>Updated Age:</label>
            <input
              type="number"
              value={updatedAge !== undefined ? updatedAge : ""}
              onChange={(e) => setUpdatedAge(parseInt(e.target.value, 10