「【React/Redux/TypeScript】実践的なフォームの扱いを完全に理解しよう!」やってみた

はじめに

【React/Redux/TypeScript】実践的なフォームの扱いを完全に理解しよう!を購入したのでこちらで学習の記録を残していこうと思います。

完成版のコード

✅ゴール
・React+TSの理解を深める

✅環境
Windows

✅参考

💻【React/Redux/TypeScript】実践的なフォームの扱いを完全に理解しよう!

💻React, Redux 初心者が、Hooks 時代の React, Redux, React-Redux に触れてみて感じたこと

💻Hooks – React Redux(公式)

useSelectorやuseDispatchなどについて記載されている。

React HooksとRedux Hooksを使ってみた

制作において

✅環境

frontディレクトリでyarn startでプロジェクト起動。

✅ディレクトリ構成

📁src
├📝index.tsx:ベース。Appにstoreを組み込んで表示。
├📁components:表示部分※useSelectorなど関数に型定義
│├📝App.tsx:Appコンポーネント
│├📝Address.tsx:住所
│├📝Basic.tsx:基本情報
│├📝Profile.tsx:プロフィール全体のレイアウトや保存の処理
│├📝College.tsx:学歴
│└📝Career.tsx:職歴
├📁domain/entity:TSのデータ型定義  ※domainは領域、entityは概念
│├📝alert.ts
│├📝proile.ts:ここにプロフィールの型を集約※Profileでエクスポート
│├📝gender.ts
│├📝address.ts
│├📝career.ts
│├📝college.ts
│├📝validation.ts:全てのProfileの入力値に対応
│└📝rootState.ts:全ての型定義をエクスポート、storeの型
├📁domain/services:ロジックの定義(ラベルの定数の定義/正規表現)
│├📝profile.ts:各項目名※PROFILEでエクスポート。タグ内のvalue属性などに利用。
│├📝address.ts:郵便番号の正規表現※APIの自動補完周りのロジック
│└📝career.ts:空の職歴がないかどうか判定するロジック
└📁store:状態管理(actions.ts,reducer.tsを各ファイルに生成)※TS型適用。
  ├📁alert
  │├📝actions.ts
  │└📝reducer.ts
  ├📁colleges
  │├📝actions.ts
  │├📝reducer.ts
  │└📝effects.ts:非同期アクションの実装
  ├📁profile
  │├📝actions.ts
  │├📝reducer.ts
  │└📝effects.ts:非同期アクションの実装
  ├📁validation
  │├📝actions.ts
  │├📝reducer.ts
  └📝index.ts:1つにstoreをまとめてエクスポート

 

actions.tsでアクションを定義。reducer.tsで初期値の作成とアクションを読み込み。

✅ライブラリなど

①typescript-fsa

ReduxをTypeScriptで記述するのは大変なので、Redux with TypeScriptの開発ではデファクトとなっているtypescript-fsaを利用。

👇以下使用例

actionCreatorFactory関数 参考

profile/actions.ts

const actionCreator = actionCreatorFactory();
~~~~
const profileActions = {
  setProfile: actionCreator<Partial<Profile>>("SET_PROFILE"),

actionCreatorにはジェネリクス(型引数)が使われている。setProfileという action のpayload(reducer に渡す値)の型をこれで定義することができる。

actionCreator.async() 参考

※非同期処理の型宣言に対応

profile/actions.ts

searchAddress: actionCreator.async<{}, Partial<Address>, {}>("SEARCH_ADDRESS")

actionCreator.async()を使うことで、非同期処理用のstart、done、failの 3 つの action を作成することが可能。
generics の3つの型引数はstart、done、failに対応しており、そのときにどんな型の payload を渡すのか定義できる。

reducerWithInitialState関数 参考

profile/reducer.ts

const init: Profile = {
  name: "",
  description: "",
  birthday: "",
  gender: ""
};

const profileReducer = reducerWithInitialState(init).case(
  profileActions.setProfile,
  (state, payload) => ({
    ...state,
    ...payload
  })
);

最初にstateの初期値を定義してreducerWithInitialState関数に引数として渡す。.case()チェーンでアクションの処理を記述。第1引数にアクション、第2引数にコールバック関数(第1引数にstateそのもの、第2引数にアクションから渡ってきたpayload)。

🙄作っていてこのライブラリ様様な気がした。別途要学習。

②Redux hooks

connect()の使い方が厄介なのでreact-reduxのhooks APIを活用する。

③redux-thunk 

非同期処理に活用。

src/store/index.ts

import { createStore, combineReducers, applyMiddleware, compose } from "redux";

🙄Reduxライブラリからインポートしている点が印象的。

applyMiddlewareはredux-thunkという外部ライブラリをreduxに登録するためのもの。

composeはRedux Dev Toolとmiddlewareをまとめてstoreに登録するもの。

redux-thunk では、dispatch 関数を引数にとる関数を返す関数(高階関数)を非同期 action として扱う。

src/store/profile/effects.ts 参考

export const searchAddressFromPostalcode = (code: string) => async (
  dispach: Dispatch
) => {
  // ...
};

dispatch を引数にとる関数を返す高階関数。

※コード割愛部分

fetchを用いて非同期のリクエストを実行。awaitを使うことで手続き的に Promise を解決。

📝印象的だった点

①interfaceとtypeの使い分けは曖昧だということ

※typeの場合

export type Profile = {
  name: string;
  description: string;
  birthday: string;
};

※interfaceの場合

export interface Profile {
  name: string;
  description: string;
  birthday: string;
}

🙄typeだと=が必要なのがわかりますね。

②Redux with hooks

Reduxで専用のhooksが用意されているのは初めて知りました(useSelector)。

基本的にconnectはもう使わないとのこと。

import React from "react";
import { useSelector } from "react-redux";

export const CounterComponent = () => {
const counter = useSelector(state => state.counter);
return <div>{counter}</div>;
};

これでstoreから状態を参照できる。

ディスパッチも以下のような形で参照できる。

const dispatch = useDispatch();

③APIの活用

PostalcodeJP API:住所補完

※2章終えた時点で自動補完のAPIが実装される。

✅備考

・ReactではTSのClass型の記法について覚える必要はあまりない

サーバー側の環境構築

このプロジェクトは直下にfront,serverディレクトリを置いています。

教材では扱っていませんでしたが、serverディレクトリ側を少し分析してみたいと思います。

index.ts

import express from "express";
import fetch from "node-fetch";
require("dotenv").config();
const app = express();

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
  res.header(
    "Access-Control-Allow-Headers",
    "Content-Type, Authorization, access_token"
  );

  // intercept OPTIONS method
  if ("OPTIONS" === req.method) {
    res.send(200);
  } else {
    next();
  }
});

app.get("/hc", (_req, res) => {
  res.send("ok");
});

app.get("/colleges", (req, res) => {
  (async () => {
    try {
      const url = `http://webservice.recruit.co.jp/shingaku/school/v1/?key=${process.env.API_KEY}&format=json&name=${req.query.name}`;
      const result = await fetch(encodeURI(url)).then(res => res.json());
      res.json(result);
    } catch (e) {
      res.status(500).send(e);
    }
  })();
});

app.listen(18001);

1文目でExpressを使っていることがわかります。

✅CORS対応

また、ヘッダーに以下の情報を付与してますね。

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
  res.header(
    "Access-Control-Allow-Headers",
    "Content-Type, Authorization, access_token"
  );

これでCORSへの対処をされている感じですね。

Access-Control-Allow-Origin

指定されたオリジンからのリクエストを行うコードでレスポンスが共有できるかどうかを示す。

*資格情報がないリクエストでは、リテラル値 “*” をワイルドカードとして指定することができます。この値はブラウザーに、すべてのオリジンからのリクエストコードにリソースへのアクセスを許可するように指示します。資格情報がある時にワイルドカードを使用すると、エラーを返します

<origin>オリジンを指定します。1つのオリジンだけを指定することができます。

Access-Control-Allow-Methods

プリフライトリクエストのレスポンスの中で、リソースにアクセスするときに利用できる1つまたは複数のメソッドを指定

Access-Control-Expose-Headers

レスポンスの一部としてどのヘッダーを公開するかを、その名前を列挙して示す

✅ルーティング

app.get("/hc", (_req, res) => {
  res.send("ok");
});

app.get("/colleges", (req, res) => {
  (async () => {
    try {~~~~

上記で/hcと/collegesが来た際に処理される内容が記述されています。

🙄Laravelでいうコントローラーみたい

✅サーバー側のポート番号

app.listen(18001);

localhost:18001で起動することがわかります。

起動

npm run start

確認

http://localhost:18001/collegesへアクセスするとJSONファイルが取得できていることがわかります。

🙄意外にexpress側の記述はシンプルなことがわかりました。

✅React側の記述

front/src/store/effects.ts

import { Dispatch } from "redux";
import collegesActions from "./actions";

export const searchColleges = (name: string) => async (dispach: Dispatch) => {
  const url = `http://localhost:18001/colleges?name=${name}`;
  const result = await fetch(url).then(res => res.json());
  dispach(
    collegesActions.searchCollege.done({
      result: result.results.school,
      params: {}
    })
  );
};

こっちもとてもシンプル。

Proxyの設定とかせずに18001ポートを直接参照できています。

記事まとめ

Re-ducksパターン:React + Redux のディレクトリ構成ベストプラクティス

【初学】初めてのAPI(fetchとかpromiseとか良くわからない)

fetchでデータを取ると、返り値としてPromiseを返す。

Fetch を使う

fetch() の最も簡単な使い方は1つの引数で取得したいリソースへのパスのみをとり、レスポンス(Responseオブジェクト)を含むpromiseを返す。
※JSONではないため、json()メソッドを併用する。

Promise(MDN)

Promise オブジェクトは非同期処理の最終的な完了処理 (もしくは失敗) およびその結果の値を表現。

非同期の状態としてreject(失敗)とresolved(成功)があって、
成功時は.then以降の処理を実行させ、失敗時は.catch以降の処理を実行できる模様。

おわりに

フォームを作るだけでもかなりボリューミーな内容。

ReduxのuseDispatchとuseSelectorは別のアプリでも使いながら勉強したいと思った。

6/6追記:

一旦完成。

別途TypeScript部分のコードを深掘りして、しっかりと理解していきたい。

完成リポジトリ

備忘録

①2-4終えた時点での自動補完がされない。

handlePostalcodeChangeが読み込まれなかったのが原因。

完成リポジトリを参考に修正したら解決。

コメントを残す