Skip to content

React Hook Formを使ったデータ送信

React Hook Formを使って、画面上に入力した値をAPIに送信する方法を記載します。サンプルとして、簡単なログイン画面を実装します。

ライブラリのインストール

npm install react-hook-form

ディレクトリ構成

.
└── src
    └── features
        └── auth
            ├── authApi.ts
            ├── authSlice.ts
            ├── authThunks.ts
            └── Auth.tsx

実装

features/auth/authApi.ts

APIの仕様に合わせたモデルLoginApiRequestおよびLoginApiResponseを使ってAPIを呼び出します。

/**
 * ログインAPIのリクエストモデルです。
 */
type LoginRequest = {
  name: string;
  password: string;
};

/**
 * ログインAPIからの正常レスポンスを格納するモデルです。
 */
type LoginSuccess = {
  ok: true;
  valid: boolean;
};

/**
 * ログインAPIからの以上レスポンスを格納するモデルです。
 */
type LoginError = {
  ok: false;
  message: string;
};

/**
 * ログインAPIのレスポンスモデルです。
 */
type LoginResponse = LoginSuccess | LoginError;

/**
 * ログインAPIを呼び出します。
 *
 * @param req ログイン情報
 * @returns 認証可否
 */
export const login = async (req: LoginRequest): Promise<LoginResponse> => {
  const url = new URL("/api/v1/login", window.location.origin);

  const res = await fetch(url.toString(), {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(req),
  });

  const data = await res.json();

  if (!res.ok) {
    return { ok: false, message: data.message };
  }

  return { ok: true, valid: data.valid };
};

features/auth/authThunks.ts

画面仕様に合わせた型LoginFormからLoginApiRequestにデータを詰め替えてAPI呼び出し関数を実行します。

import { createAsyncThunk } from "@reduxjs/toolkit";

import { login } from "./authApi";

/**
 * ログインリクエスト情報を画面から渡すモデルです。
 */
export type LoginForm = {
  name: string;
  password: string;
};

/**
 * ログイン成功時の状態をactionに渡すモデルです。
 */
type LoginSuccess = {
  valid: boolean;
};

/**
 * ログイン失敗時の状態をactionに渡すモデルです。
 */
type LoginError = {
  message: string;
};

/**
 * ログインAPIを呼び出して返ってきた結果をstateに保持します。
 */
export const loginThunk = createAsyncThunk<
  LoginSuccess,
  LoginForm,
  { rejectValue: LoginError }
>("auth/login", async (form, { rejectWithValue }) => {
  try {
    const res = await login({
      name: form.name,
      password: form.password,
    });

    if (!res.ok) {
      return rejectWithValue({ message: res.message });
    }

    return { valid: res.valid };
  } catch {
    return rejectWithValue({ message: "不明なエラーが発生しました。" });
  }
});

features/auth/authSlice.ts

API呼び出し時の状態管理をextraReducersで行います。

import { createSlice } from "@reduxjs/toolkit";

import { loginThunk } from "./authThunks";

/**
 * ログインの状態を保持するstateです。
 */
type AuthState = {
  valid: boolean;
  message?: string;
};

const initialState: AuthState = {
  valid: false,
  message: undefined,
};

const authSlice = createSlice({
  name: "auth",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      /**
       * ログインAPI呼び出し開始時の状態遷移
       */
      .addCase(loginThunk.pending, (state) => {
        state.valid = false;
        state.message = "ログイン中...";
      })
      /**
       * ログインAPI呼び出し正常終了時の状態遷移
       */
      .addCase(loginThunk.fulfilled, (state, action) => {
        state.valid = action.payload.valid;
        state.message = action.payload.valid ? "ログイン完了" : "ログイン失敗";
      })
      /**
       * ログインAPI呼び出し異常終了時の状態遷移
       */
      .addCase(loginThunk.rejected, (state, action) => {
        state.valid = false;
        state.message = action.payload?.message;
      });
  },
});

export default authSlice.reducer;

features/auth/Auth.tsx

入力された認証向けのデータをLoginFormにセットします。

import { useForm } from "react-hook-form";

import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { type LoginForm, loginThunk } from "./authThunks";

/**
 * ログイン画面のコンポーネントです。
 *
 * @returns ログイン画面コンポーネント
 */
export const Auth = () => {
  const { register, handleSubmit } = useForm<LoginForm>();
  const authState = useAppSelector((state) => state.auth);
  const dispatch = useAppDispatch();

  return (
    <div>
      <form
        onSubmit={handleSubmit((form: LoginForm) => dispatch(loginThunk(form)))}
      >
        <div>
          <input
            aria-label="name"
            {...register("name")}
            type="text"
            placeholder="name"
          />
        </div>
        <div>
          <input
            aria-label="password"
            {...register("password")}
            type="password"
            placeholder="password"
          />
        </div>
        <div>
          <button type="submit">ログイン</button>
        </div>
        {authState.message && <div>{authState.message}</div>}
      </form>
    </div>
  );
};