20200224のReactに関する記事は14件です。

GitHub ActionsでReactアプリをS3にデプロイ(+ Cloud Frontのキャッシュクリア)して、Slackに通知するまで

以前から業務でReactを使用していましたが、デプロイは手動でやっていたため、自動化できたらいいなという話をチームでしていました。
そこで、正式リリースもされたGitHub Actionsをせっかくなので使ってみたいなと思い、自動化に挑戦してみました。

GitHub Actionsとは?

簡単に言うとGitHubが公式で公開している機能で、トリガーをきっかけにあらかじめ定義しておいた処理を実行するというものです。はじめはベータ版で公開されていましたが、2019年11月に正式公開されました。
いわゆるCI/CDが実現できます。

これまではCircle CIなど、外部サービスと連携させて行うことが多かったですが、GitHub Actionsを使用することで、GitHubだけで実現させることができます。

こちらの記事で概要から機能まで幅広く解説されています。
- GitHubの新機能「GitHub Actions」で試すCI/CD


※以下、AWSアカウント、Slackワークスペースがある前提で進めていきます。


S3の準備

GitHub Actionsでデプロイする先を用意します。
マネジメントコンソールでS3の画面へ。

バケットの作成

  1. 「バケットを作成する」を選択
  2. 名前とリージョン
    • バケット名を入力(全世界で一意の名前の必要があります)
    • リージョンを選択
    • 「次へ」
  3. オプションの設定
    • 任意で設定(そのまま「次へ」でもOK)
  4. アクセス許可の設定
    • 4つともオフにする(本来は適度にオンがいいと思いますが、とりあえず動作させたいのでオフにしました)
    • 「次へ」
  5. 確認
    • 確認して問題なければ「バケットを作成」

静的サイトとして公開

  1. 作成したバケットを選択
  2. プロパティタブ → Static website hosting を選択
  3. 「このバケットを使用してウェブサイトをホストする」を選択し、インデックスドキュメントに「index.html」を入力
  4. 「保存」を選択

その後、バケットホスティングと表示されていれば公開されています。
なお、バケット名はポリシー作成で使用するので控えておきます。

本来はここで、サイトへのアクセスを受け入れるために以下のようなバケットポリシーを設定します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{※バケット名}/*"
        }
    ]
}

ですが、今回はCloud Frontを経由してのみアクセスできるようにするために、あとで設定を上書きするので、この場では何も設定しません。

ちなみにバケット作成時にブロックパブリックアクセスを4つすべてオフにしていますが、オフにしたからといって、誰でも自由にアクセスできるようになるということではありません。
バケットポリシーで許可されたアクセスであったり、付属ポリシーで許可されたIAMユーザのアクセスであれば可能です。

Cloud Frontの準備

ホスティング自体はS3だけで可能ですが、CDNを前に置きたいので設定します。
マネジメントコンソールでCloud Frontの画面へ。

ディストリビューションの作成へ

  1. 「Create Distribution」を選択
  2. Webの「Get Started」を選択

Create Distribution

  • Origin Settings
    • Origin Domain Name:先ほど作成したバケットを選択(選択するとRestrict Bucket Accessが出現します)
    • Origin ID:Origin Domain Nameを選択すると自動的に入力される
    • Restrict Bucket Access:Yes(Yesにすると以下の項目が出現します)
    • Origin Access Identity:Create a New Identity
    • Comment:任意の名称(デフォルトのままでもOK)
    • Grant Read Permissions on Bucket:Yes, Update Bucket Policy
    • ※これ以外は任意(そのままでもOK)
  • Default Cache Behavior Settings
    • ※任意(そのままでもOK)
  • Distribution Settings
    • Default Root Object:「index.html」と入力
    • ※これ以外は任意(そのままでもOK)
  • 「Create Distribution」を選択

これでディストリビューションが作成されますが、動作するまで15分ほどかかります。
statusがIn ProgressからDeployedに変わればOKです。

ディストリビューションのARNをポリシー作成で使用するので控えておきます。
arn:aws:cloudfront::XXXXXXXXXXXX:distribution/XXXXXXXXXXXXXX のような形式です。
(ディストリビューションを選択して、Generalタブで確認できます)

補足

Create Distributionでの設定について。

Restrict Bucket AccessとOrigin Access Identity

Restrict Bucket Accessについては、S3バケットへのアクセスをClout Front経由からのみにする設定です。
この設定をするにあたって、Cloud Frontのどのディストリビューションからのみアクセスを受け入れればいいのか、S3が判断するために必要なのがOrigin Access Identity(通称:OAI)です。
OAI自体はただの文字列にすぎませんが、ディストリビューションと紐づけることで、アクセス制限をかけられます。

ちなみにRestrict Bucket AccessOrigin Domain Nameでバケットを選択することで出現しますが、うまく動作しなかったのか出現しなかったことがありました。(何度かやりなおしたら出現しました)

Grant Read Permissions on Bucket

ディストリビューションに紐づいているS3バケットのバケットポリシーを、OAIを使用したアクセス制限をかけるための設定へ上書きするかどうかです。
上書きすることで以下のようなバケットポリシーに変わります。

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity {※OAI ID}"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{※バケット名}/*"
        }
    ]
}

Default Root Object

こちらについては、S3バケットで静的サイトとして公開する際に設定しているのでは?となるかもしれませんが、Cloud Front側でも設定しておく必要があります(設定しないとブラウザからアクセスしたときにAccessDeniedになりました)

IAMの準備

GitHub Actionsでデプロイする際に使用する、デプロイ用のIAMユーザを作成します。
マネジメントコンソールでIAMの画面へ。

ポリシーの作成

  1. ポリシー一覧から「ポリシーを作成」を選択
  2. JSONに以下を記述して「ポリシーの確認」を選択
    {}の部分はこれまで控えたものに適宜置き換えてください。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::{※S3バケット名}",
                "arn:aws:s3:::{※S3バケット名}/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:GetDistribution",
                "cloudfront:GetDistributionConfig",
                "cloudfront:CreateInvalidation",
                "cloudfront:ListInvalidations",
                "cloudfront:GetInvalidation"
            ],
            "Resource": "{※Cloud FrontディストリビューションのARN}"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudfront:ListDistributions",
                "cloudfront:ListStreamingDistributions"
            ],
            "Resource": "*"
        }
    ]
}

3.ポリシーの確認
 - 任意の名前と説明を入力して「ポリシーの作成」を選択

ユーザの作成

  1. ユーザ一覧から「ユーザを追加」を選択
  2. ユーザ名は任意、アクセスの種類は「プログラムによるアクセス」のみ選択して「次のステップ」
  3. アクセス権限
    • 「既存のポリシーを直接アタッチ」を選択
    • 先ほど作成したポリシーを選択して「次のステップ」
  4. タグ
    • 任意(そのまま次のステップでもOK)
  5. 確認して「ユーザの作成」を選択
  6. アクセスキーとシークレットキーを控えておく

Slackの準備

GitHub Actionsの結果を通知するためのWebhook URLを用意します。

Slack Appの作成

  1. Slackワークスペースのワークスペース名のところからメニューを開く
  2. ビルド画面へ
    • ※ワークスペースのオーナーの場合
      その他管理項目 → Appを管理する → 右上の「ビルド」
    • ※オーナーでない場合
      Slackをカスタマイズ → 左上のメニューを開く → App管理 → 右上の「ビルド」
  3. Start Buildingを選択
    • App Name:任意の名前
    • Development Slack Workspace:任意のワークスペース
    • 「Create App」を選択

以下、作成したAppの画面で進めます。

Botの作成

Incomming Webhookを使用するために必要なので用意します。

  1. Building Apps for SlackのAdd features and functionalityを開いて、Botsを選択
  2. How Your App Displaysで「Edit」を選択
    • Display Name (Bot Name):任意の名前を入力
    • Default username:任意の名前を入力
    • 「Add」を選択

Incomming Webhook URLの作成

  1. Building Apps for SlackのAdd features and functionalityを開いて、Incomming Webhooksを選択
  2. Activate Incoming WebhooksをONにして、「Add New Webhook to Workspace」
  3. 投稿するチャンネルを選択して、「許可する」を選択

これでWebhook URLが生成されるので控えておきます。

GitHubの準備

リポジトリ

適当なリポジトリを用意します。

Reactアプリ

Reactアプリをリポジトリにpushして用意しておきます。

ちなみに自分はcreate-react-appで作れる雛形でやりました。
yarnの場合だと、yarnが使える環境で以下のコマンドでサクッと作れます。
create-react-appはグローバルインストールサポートしなくなったらしいです)

$ yarn create react-app (作成する場所のパス)

それとGitHub Actionsの中でESLintを使うので、package.jsonのscriptsに"lint": "./node_modules/.bin/eslint \"src/**/*.js\""を追加しておきます。

GitHub Secrets

GitHub Actionsで使用する環境変数を設定します。
公開したくない秘匿情報などを定義するのに向いており、GitHub Secretsに登録した変数の値は後から確認できないようになっています。

リポジトリのSettings → Secretsで以下の情報を登録しておきます。値はこれまで控えてきたものです。
(AWS_S3_BUCKET_PATHはs3://{s3のバケット名}形式です)

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • AWS_S3_BUCKET_PATH
  • SLACK_WEBHOOK_URL

なお、GITHUB_TOKENについては自動で値がセットされるので、設定しなくて大丈夫です。

GitHub Actionsの設定ファイル作成

ワークフロー定義ファイルはymlで記述して、リポジトリルート直下に.github/workflows/ワークフロー定義ファイル名.ymlで配置します。
配置する場所さえ合っていれば、ファイル名は何でもOKです。

自分の場合は以下のようになりました(Docker使ってやってました)

(プロジェクトフォルダ)
├─ .github
|  ├─ workflows
|  |   ├─ ※ワークフロー定義ファイル
├─ node_modules
|  ├─ (各種パッケージ)
├─ public
|  ├─ favicon.ico
|  ├─ index.html
|  ├─ logo192.png
|  ├─ logo512.png
|  ├─ manifest.json
|  ├─ rebots.txt
├─ src
|  ├─ App.css
|  ├─ App.js
|  ├─ App.test.js
|  ├─ index.css
|  ├─ index.js
|  ├─ logo.svg
|  ├─ serviceWorker.js
|  ├─ setupTest.js
├─ docker-compose.yml
├─ Dockerfile
├─ .gitignore
├─ package.json
├─ README.md
├─ yarn.lock

以下の内容でymlファイルを作成します。
自分の場合、ステージング環境にデプロイするイメージで作りました。

name: Staging Deploy
on:
  push:
    branches:
      - release/**
env:
  project-name: ga-test-project

jobs:
  check:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x

      - name: Get Yarn Cache Directory Path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - name: Cache Node Modules
        uses: actions/cache@v1
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-${{ env.project-name }}-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
          restore-keys: |
            ${{ runner.os }}-${{ env.project-name }}-

      - name: Package Install
        run: yarn install

      - name: Lint
        run: yarn lint

      - name: Slack Notification by NonSuccess
        uses: 8398a7/action-slack@v2
        if: success() != true
        with:
          status: ${{ job.status }}
          author_name: 'check'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

  deploy:
    name: Build & Deploy
    needs: check
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: 12.x

      - name: Get Yarn Cache Directory Path
        id: yarn-cache-dir-path
        run: echo "::set-output name=dir::$(yarn cache dir)"

      - name: Cache Node Modules
        uses: actions/cache@v1
        with:
          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
          key: ${{ runner.os }}-${{ env.project-name }}-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
          restore-keys: |
            ${{ runner.os }}-${{ env.project-name }}-

      - name: Package Install
        run: yarn install

      - name: Build
        run: yarn build

      - name: Publish to AWS S3 & CloudFront Cache Clear
        uses: opspresso/action-s3-sync@v0.2.3
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: 'ap-northeast-1'
          FROM_PATH: './build'
          DEST_PATH: ${{ secrets.AWS_S3_BUCKET_PATH }}

      - name: Slack Notification
        uses: 8398a7/action-slack@v2
        if: always()
        with:
          status: ${{ job.status }}
          author_name: 'deploy'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

いざデプロイ

今回はrelease/が接頭辞のブランチをpushすることがトリガーになっているため、これに該当する名称のブランチを作成してpushしてみましょう。
ちゃんと動作していれば、リポジトリのActionsからワークフローの状況を確認できるようになっていると思います。
それぞれのステップのログも確認できるので、もしどこかでエラーになって失敗しても原因調査に役立ちます。

無事最後まで成功していれば、Cloud Frontからデプロイしたサイトにアクセスしてみましょう。
(ディストリビューションのGeneralのDomain NameでURLが確認できます)
また、S3に直接アクセスできないことも確認してみましょう。403 Forbiddenになるでしょうか。
(プロパティのStatic website hostingからURLが確認できます)

そして、Slackにもちゃんと通知できているでしょうか?
通知の内容は以下のような感じになります。

成功
github-actions-slack-success.png

キャンセル
github-actions-slack-canceled.png

失敗
github-actions-slack-failed.png


とりあえずデプロイまで無事到達できてよかったのですが、色々検証しながらやっていたら、かなり時間を使ってました(苦笑)
まぁ、はじめてやることに時間かかるのはつきものでしょうか。
GitHub Actionsを使おうとしている方の何かの参考になれば幸いです。

ワークフロー定義ファイルに関する解説も書こうと思っていましたが、ちょっと力尽きてしまったので、また後日余裕があったら追記するかもしれません。

参考リンクまとめ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tecpitの教材「楽曲をリアルタイム検索してみよう!【React】」を試してみました / TechCommit企画

テックコミットさんのお年玉企画でTecpitさんの「楽曲をリアルタイム検索してみよう!【React】」を試させていただきましたので、情報をまとめて見ました!

教材の概要

検索したキーワードに応じて、リアルタイムでitunsAPIから情報を引用し、
キーワードにhitしたアルバムや曲情報を50件まで画面に表示させます。
実際の概要[ https://www.techpit.jp/courses/530899/lectures/9701851 ]
progateのjs ES6を一度やっておくことを推奨されています。

Reactを使い、ハンズオン形式(テキストベース)で作る教材でした。
一般公開しているAPIデータを引っ張ってくるので、データベースとかは使いません。
フロント側だけの技術で作ることができるコンテンツになります。

自分の前提知識

JavaScriptのFWはVue.js、react、redux、backboneもオンラインスクール/ネット情報でざっと基礎的な動作を学んだことがあります/
が、Vue以外は触った時間も短く、ほぼ忘れている状態ですw

この教材で学べる知識

・yarnを使ったreact関連データのインストール方法
・Reactの基本的な書き方(renderやメソッド、ライフサイクルなんか)
・Componentの作成と相互データの受け渡し
・Parcelを使ったバンドル
・axiosによる非同期でのデータの引用方法
・RsJSの概要

progateでjsを学んだだけであれば、全てが新しい知識になるので少し敷居は高いかもしれません。
ただ、それぞれの項目に基本的な概念などはきちんと書いているので「これはこういうものなんだ」という概要理解までは問題なくできると思います。

yarnは何度か使ったことがありますが、バンドルは今まではvue.jsやreactを使うときにはwebpackとかで構築してたのでparcelという簡易的なものがあるんだーということを知れて良かったです。
あとは検索ボタンで検索ではなく、文字入力で非同期検索を少し時間差で行う処理をRsJSというものでやっています。
これも初めて知りました。うっすらと覚えときたい!

少しカスタマイズしたところ/実際の動き

itunesの検索結果は50件までしか取得できないのですが、
せっかくなので11件以上の検索結果があれば「次へ」「前へ」を出すようなページネーションの動作を行いました。APIのhit件数に応じて、ページネーションコンポーネントを出すかどうかをif判定しているような感じです。
参考: http://bashalog.c-brains.jp/19/10/04-164802.php

ezgif.com-video-to-gif.gif
フロントの表示は、背景画像や2列並び、ページネーション出すようにしたくらいのカスタマイズになります。

全体の感想

環境構築について、parcelなんかでのバンドル方法は見たことがなかったのですが
シンプルな反面、キャッシュファイルを大量に作るなど癖もあるような印象もあったので、
初めてやる人でも、参考情報が多いイメージのあるwebpackでの構築でも良いのかなーと思いました。

良かったところとして、前述したように、それぞれの説明が概要理解まではできるようになっており、また説明もわかりやすかったです。
特にコンポーネント間の関連性の説明がわかりやすく、より理解を深められたと思います。

ただ、もちろん概要までなので、きちんと自分の中に落とし込むにはドキュメントなどの情報も読んだり、自分でも何度か作って見る必要はあると思います。

また、記述ミスや、公開後の処理についてうまくいかないところがあり質問なども結構たくさん投げているのですが、きちんと丁寧に答えていただき、とてもありがたいなと思いました!

お読みいただき、ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Testable Redux ~ React と Redux のテスト戦略 ~

image

本記事では Redux を使用した場合の React コンポーネントに対するテスト方法を考察します。

Redux に接続された React コンポーネントは、コンポーネント内のプロパティを Redux Store の state と同期させています。 Redux に強く依存しているコンポーネントをどのように Testable にしていけば良いのでしょうか。記事のタイトルは、t-wada 先生の言葉をお借りして、Testable Redux とつけさせていただきました。

純粋な React コンポーネントをテストする

さて、まずはじめに純粋な React のコンポーネントに対するテスト方法を振り返ってみます。Facebook に習えば、jest の公式ドキュメントでも紹介されている jestenzyme を組み合わせた方法が一般的でしょう。

以下のようなシンプルなコンポーネントを例として使用します。

next

count を保持する state をもち、onClick で state を変更します。簡単のために Functional Component とし、State Hooks を使用します。後のテストのために各タグにはカスタムデータ属性(data-test)を付与しておきます。プロダクションビルドでカスタムデータ属性を取り除く方法もあります。こちらの記事にて詳細に解説されていました。

import React, { useState } from "react";
export default () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h3 data-test="count">count: {count}</h3>
      <button data-test="count-up" onClick={() => setCount(count + 1)}>⬆︎</button>
      <button data-test="count-down" onClick={() => setCount(count - 1)}>⬇︎</button>
    </div>
  );
};

これに対するテストは以下のように記述できます。初期値が 0 であること、ボタンをクリックして 1 に変わることを確認します。

import React from "react";
import { shallow } from "enzyme";
import Counter from "./PureReactCounter";

import testConfigure from "../../testConfigure";
testConfigure();

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  const Component = shallow(<Counter />);
  it("ボタンをクリックしてカウントアップする", () => {
    expect(Component.find("h3").text()).toEqual("count: 0");
    Component.find(sel("count-up")).simulate("click");
    expect(Component.find("h3").text()).toEqual("count: 1");
  });
});

なお、testConfigure には以下を記述しておきます。enzyme を使用する場合のお約束のようなものです。enzyme の installationにて解説されています。

import Enzyme from "enzyme";
import EnzymeAdapter from "enzyme-adapter-react-16";
export default () => Enzyme.configure({ adapter: new EnzymeAdapter() });

結果は 8ms でした。このテストでは shallow() を使用しています。Shallow レンダリングは、コンポーネントをユニットテストの範囲でテストできるように制限します。テストが子コンポーネントの動作を間接的にアサートしないようにしてくれます。コンポーネントは子コンポーネントに依存させず、常にステートレスに保つことによって Shallow レンダリングによるテストができ、高速です。

 PASS  src/components/PureReactCounter.test.tsx
  <Counter /> コンポーネント
     ボタンをクリックしてカウントアップする (8ms)

ちなみに、mount() は完全な DOM レンダリングを行います。DOM API とやり取りする可能性のあるコンポーネントである場合や、より高次のコンポーネントにラップされているコンポーネントをテストする必要がある場合に最適です。ただしその一方で多くの依存関係を考慮することによりテストに要する時間は増える傾向にあります。

 PASS  src/components/PureReactCounter.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
       ボタンをクリックしてカウントアップする (8ms)
    Fullレンダリング
       ボタンをクリックしてカウントアップする (8ms)

結果として実行時間はほとんど変わりませんでした。これは今回実装したコンポーネントが他のコンポーネントや API などと依存していないシンプルなコンポーネントであるためです。

Redux の使用を開始する

さて、先ほど実装したコンポーネントは、count を保持する state をもち、onClick で state を変更していました。この count という state を Redux の store で管理します。実装例を紹介する前に Redux のライフサイクルのおさらいをしましょう。

  1. View: ユーザが操作を行い、handleClick() などのイベント起動 function が実行される。
  2. ActionCreater: イベント起動 function は ActionCreater を通して action を生成する。
  3. Dispacther: action は Dispater に渡され、Reducer に流れる。
  4. Reducer: Reducer は action の type に応じて新しい state を返却する。

lifecycle

説明の粒度は荒いですが、おおまかにこのような流れで Redux による状態の管理が行われます。

さて、ディレクトリ構成は以下のようにして実装を進めていきます。

src/
├── index.tsx
├── App.tsx
├── components
│   ├── Counter.tsx
│   └── Counter.test.tsx
└── store
    ├── actions
    │   └── counterActions.ts
    ├── reducers
    │   └── counterReducer.ts
    └── store.ts

Action (actions/counterActions.ts)

Action の定義を行います。今回はカウントアップとカウントダウンする2種類の action があるので事前に定義しておき、他のファイルから参照できるようにしておきましょう。

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

export interface ICountReducerAction {
  type: typeof INCREMENT | typeof DECREMENT;
}

Reducer (reducers/counterReducer.ts)

Reducer は飛んできた action の type に応じて state 返却する処理を書くのでした。INCREMENT の場合は +1DECREMENTの場合は -1された新しい state を返却しています。

import {
  ICountReducerAction,
  INCREMENT,
  DECREMENT
} from "../actions/counterActions";

const initialState = 0;
const counterReducer = (state = initialState, action: ICountReducerAction) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
};
export default counterReducer;

Store

先に定義した reducer を束ねて、storeを作成しましょう。今回 reducer は1つですが、複数使用することになることも考慮して combineReducers() を使用しています。

import { createStore } from "redux";
import { combineReducers } from "redux";
import counterReducer from "./reducers/counterReducer";
const reducer = combineReducers({ count: counterReducer });
export default createStore(reducer);

Component から Redux の store を参照する

Redux の state を View に表示できるようにコンポーネントを実装していきましょう。まず初めに純粋に Redux の store を直接参照するような実装を考えます。

ReactDOM.render()store から subscribe() します。

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./store/store";

const render = () => ReactDOM.render(<App />, document.getElementById("root"));
render();
store.subscribe(render);

コンポーネントは以下のように実装します。この場合はどうでしょう、テストは容易でしょうか。

import React from "react";
import store from "../../store/store";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

export default () => {
  const count = store.getState().count; // store から直接参照する
  return (
    <div>
      <h3 data-test="count">count: {count}</h3>
      <button onClick={() => store.dispatch({ type: INCREMENT })}>⬆︎</button>
      <button onClick={() => store.dispatch({ type: DECREMENT })}>⬇︎</button>
    </div>
  );
};

テストコードを振り返ってみましょう。このままでは Redux の store を参照ができないため、ボタンをクリックした後に発火するはずの Reducer が起動しません。結果として count は 0 のままです。

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const Component = shallow(<Counter />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(Component.find("h3").text()).toEqual("count: 0");
      Component.find(sel("count-up")).simulate("click"); // Redux store の state が変更されない
      expect(Component.find("h3").text()).toEqual("count: 1"); // 0 のまま
    });
  });
  describe("Fullレンダリング", () => {
    const Component = mount(<Counter />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(Component.find("h3").text()).toEqual("count: 0");
      Component.find(sel("count-up")).simulate("click");
      expect(Component.find("h3").text()).toEqual("count: 1");
    });
  });
});

テスト結果(クリックして開く)
 FAIL  src/components/DirectAccessReduxStore.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
      ✕ ボタンをクリックしてカウントアップする (11ms)
    Fullレンダリング
      ✕ ボタンをクリックしてカウントアップする (6ms)

  ● <Counter /> コンポーネント › Shallowレンダリング › ボタンをクリックしてカウントアップする

    expect(received).toEqual(expected) // deep equality

    Expected: "count: 1"
    Received: "count: 0"

      13 |       expect(Component.find("h3").text()).toEqual("count: 0");
      14 |       Component.find(sel("count-up")).simulate("click");
    > 15 |       expect(Component.find("h3").text()).toEqual("count: 1");
         |                                           ^
      16 |     });
      17 |   });
      18 |   describe("Fullレンダリング", () => {

      at Object.<anonymous> (src/components/DirectAccessReduxStore.test.tsx:15:43)

  ● <Counter /> コンポーネント › Fullレンダリング › ボタンをクリックしてカウントアップする

    expect(received).toEqual(expected) // deep equality

    Expected: "count: 1"
    Received: "count: 0"

      21 |       expect(Component.find("h3").text()).toEqual("count: 0");
      22 |       Component.find(sel("count-up")).simulate("click");
    > 23 |       expect(Component.find("h3").text()).toEqual("count: 1");
         |                                           ^
      24 |     });
      25 |   });
      26 | });

      at Object.<anonymous> (src/components/DirectAccessReduxStore.test.tsx:23:43)

Redux と React のコンポーネントが強く依存する関係になっており、React のコンポーネント単体としてテストしづらくなっています。この構成を変えていきましょう。まずは React と Redux の間の依存関係を切り離す方法を考えます。

react-reduxconnect() を使用して React と Redux の依存を引き剥がず

最も代表的な実装パターンとして react-reduxconnect() を使用する方法が挙げられます。これは、コンポーネントをマウントする時点で子コンポーネントや依存関係にある構成要素を考える必要がなく、シンプルです。

Counter コンポーネントと Redux store が強い依存関係にある実装から、なるべく疎結合になるように配慮します。いわゆる DI (Dependency Injection) の考え方にしたがって、Counter コンポーネントの store を外から props として注入できるように変更します。ただ単に props によって注入するだけではなく、Redux store との接合は connect() によって行います。詳細な手続きは公式チュートリアルを参照しましょう。このようなコンポーネントを HOC (Higher Order Component) と呼びます。詳細はこちらの記事が参考になります。

import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

interface ICounterState {
  counter: number;
}

interface ICounterProps {
  count: number;
  increment: any;
  decrement: any;
}

// props として store を流し込んで DI できるるようにする。
export const Counter = (store: ICounterProps) => {
  const { count, increment, decrement } = store;
  return (
    <div>
      <div data-testid="count">count: {count}</div>
      <button onClick={increment}>⬆︎</button>
      <button onClick={decrement}>⬇︎</button>
    </div>
  );
};

const mapStateToProps = (state: ICounterState) => ({
  count: state.counter
});
const mapDespatchToProps = (dispatch: Dispatch) => ({
  increment: () => dispatch({ type: INCREMENT }),
  decrement: () => dispatch({ type: DECREMENT })
});

// 第一引数の mapStateToProps は component に渡す props を制御する
// 第二引数の mapDespatchToProps は reducer を呼び出して、redux で管理している state を更新する
// Counter は取得したデータを props として扱いたい component を指定する
export default connect(mapStateToProps, mapDespatchToProps)(Counter);

さて、ここまでできれば勝てる気がしてきました。テストを書きます。

import React from "react";
import { mount, shallow } from "enzyme";
import { Provider } from "react-redux";
import { createStore } from "redux";
import sinon from "sinon";
import ConnectedCounter, { Counter } from "./ReactRedux";
import { reducer } from "../../store/store";
import testConfigure from "../../testConfigure";
testConfigure();
const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const props = {
      count: 0,
      increment: sinon.spy(), // カウントアップするボタンを押した挙動を確認するためにスパイを差し込む
      decrement: sinon.spy()
    };
    const shallowComponent = shallow(<Counter {...props} />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(shallowComponent.find("h3").text()).toEqual("count: 0");
      shallowComponent.find(sel("count-up")).simulate("click");
      expect(props.increment).toHaveProperty("callCount", 1);
    });
  });
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

上記のテストでは、Shallow レンダリングと Full レンダリングの2つの方法でテストを記述しました。

Shallow レンダリングでは Redux の store を含めず、コンポーネント単体でテストを行います。そのため、カウントアップするボタンを押した際のハンドラ関数は simon.spy() を使用してスパイを差し込んで挙動を確認します。

一方で、Full レンダリングの場合は Redux の store だけをモックとして作成し、<Provider>の store に DI します。こちらの手法の方が Redux の store を含めたテストにはなるのですが、実行時間が増える傾向にあるため注意が必要です。ちなみに実行時間を比較すると以下のようになります。

 PASS  src/components/container/ReactRedux.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
       ボタンをクリックしてカウントアップする (9ms)
    Fullレンダリング
       ボタンをクリックしてカウントアップする (44ms)

mount するのはインテグレーションテストだユニットテストではない、shallow の方が高速だ、などの論争があります。個人的な意見ですが、チーム開発を行う上ではシンプルかつ可読性を維持したテストコードを書いた方がむしろチームとしてアジリティが上がるのではないかと考えています。学習コストも高いですしね。
それでもプロダクトコードの量が大きくなるにつれて、テストの実行時間の増加によりアジリティが下がるケースもあります。shallow レンダリングと full レンダリングの実行時間の差は ms 程度ですが、塵も積もれば山となるということです。テストの方針を決めるのは難しいですね。

Redux Hooks を使用してコード量を減らす

怠惰で傲慢な我々は、現状に満足することはありません。上に挙げた実装は React と Redux の依存関係を引き剥がすことで、Shallow レンダリングによるテストが実現できるようになりました。その一方で少し複雑な実装をしなければならないように感じます。この章では Redux Hooks を使用して簡単に記述できる方法をご紹介します。

React の Hooks API は、Functional コンポーネントに対してローカルコンポーネントの state を使用できるようになる優秀な機能です。react-redux でも、既存の connect() を使用して実装された HOC (Higher Order Component) の代わりとして Hooks API を提供するようになりました。これを使用すると、前章の実装のようにコンポーネントをにラップして HOC を作るような面倒な作業は必要ありません。Redux store に subscribe して action を dispatch できます。

導入は非常に簡単です。useSelector()useDispatch() を使用しましょう。

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

export default () => {
  const count = useSelector((state: any) => state.count);
  const dispatch = useDispatch();
  const increment = () => { dispatch({ type: INCREMENT })};
  const decrement = () => { dispatch({ type: DECREMENT })};
  return (
    <div>
      <h3>count: {count}</h3>
      <button data-test="count-up" onClick={increment}>⬆︎</button>
      <button data-test="count-down" onClick={decrement}>⬇︎</button>
    </div>
  );
};

コードがとても短くなりました。かなりスマートです。あれ? でもおかしいですね、少し振り返ってみましょう。connect() を使用して HOC を作ったときには純粋な React コンポーネントと Redux との依存関係を引き剥がすことに成功していました。今回、Redux Hooks を使用した実装では、また依存関係が強くなってしまいました。これでは Full レンダリングを行うようなテストしか記述できません。

describe("<Counter /> コンポーネント", () => {
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

Redux Hooks でも Redux と React を分離する

基本的な考え方は今までと同様です。Redux と接続する部分を DI できるようにすれば OK です。useSelector()useDispatch() を使用している箇所を外に出してやりましょう。

import { useSelector, useDispatch } from "react-redux";
import { IRootState } from "../../store/store";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";
import { Dispatch } from "redux";

interface ICounterProps {
  count: number;
  increment: Dispatch;
  decrement: Dispatch;
}
export const Counter = (props: ICounterProps) => {
  const { count, increment, decrement } = props;
  return (
    <div>
      <h3>count: {count}</h3>
      <button data-test="count-up" onClick={increment}>⬆︎</button>
      <button data-test="count-down" onClick={decrement}>⬇︎</button>
    </div>
  );
};

export default (props: any) => {
  const count = useSelector<IRootState>(state => state.count);
  const dispatch = useDispatch();
  const _props = {
    count,
    increment: () => dispatch({ type: INCREMENT }),
    decrement: () => dispatch({ type: DECREMENT }),
    ...props
  };
  return <Counter {..._props} />;
};

当然といえば当然ですが、テストコードは react-reduxconnect() を使用した場合の実装と同じになります。

テストコード(クリックして開く)

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const props = {
      count: 0,
      increment: sinon.spy(),
      decrement: sinon.spy()
    };
    const shallowComponent = shallow(<Counter {...props} />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(shallowComponent.find("h3").text()).toEqual("count: 0");
      shallowComponent.find(sel("count-up")).simulate("click");
      expect(props.increment).toHaveProperty("callCount", 1);
    });
  });
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

さいごに

結局のところ、どのアプローチを採用した場合も実際の Redux store を使用した React コンポーネントのテストは高速です。Redux の設計は、Action、Reducer、State のそれぞれが互いに分離される方法のため、テストに非常に適しています。
このように Dependency Injection しやすい設計のもと Redux が作られているので、このようなシンプルな構造を取ることができました。今回はご紹介できませんでしたが、Redux SagaRedux Thunk を使用した場合のアーキテクチャでも同様にテストはシンプルに記述できます。React をとりまくエコシステムは素晴らしいですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】HooksとContextで作るモダンReactアプリ

はじめに

モダンReactでの開発ではHooksとContextを使うことが多くなってきたと思います。(大規模SPAではReduxが活きるケースも多そうですが。)
特にHooksを用いた関数コンポーネントを使うのはもはや必須で、特別な理由がない限りクラスコンポーネントを使うことはなくなりました。
今回はHooksとContextを使った小さなアプリを作りながら、その使い方とメリット・デメリットについて書いていきたいと思います。

筆者のReact歴

実務で半年、個人開発で使い始めてから1年といったところです。
ちょうどHooksがリリースされて頃ですね。
保守でクラスコンポーネントを修正することはありますが、新規開発ではもっぱらHooksを用いて関数コンポーネントを使っています。

実際のアプリとコード

エンジニア向けにツイッターのヘッダー用画像を作成する「iCard」というアプリを作成しました。
React + TypeScriptで作成し、Firebaseにデプロイしています。
サイトURL
GitHubリポジトリ
image.png
「技術分野」と「技術スタック」を入力して、スクリーンショットを撮ることでツイッターのヘッダー用画像を作成することができます。

HooksとContextの概要

Hooks

今までStateを管理したり、ライフサイクルメソッドを使ったりできるのはクラスコンポーネントだけでしたが、それを関数コンポーネントでも可能にしたのがHooksです。(ライフサイクルについては、Hooksではほぼ意識することなく開発できるようになった。)
Hooksを使って関数コンポーネントで開発すると、関連する機能に基づいて、1つのコンポーネントを複数の小さな関数に分割することができ、保守性・可読性の高いコードを書くことができます。
公式ドキュメント - フックの導入

Context

Contextを使うとコンポーネントツリーの中でグローバルにデータを管理することができます。
親コンポーネントで定義したContextを子や孫コンポーネントで簡単に参照できます。
大雑把に言うと簡易版Reduxのようなものです。
このContextがHooksの登場(useContext)によって大幅に使いやすくなっています。
公式ドキュメント - コンテクスト

実例

useState

useStateはLocal Stateを管理するために使います。
公式ドキュメント - useState

iCardではフォームに入力された値の管理に使っています。

src/app/components/Form.tsx
import { useState } from 'react';

const [technologyFieldForm, setTechnologyFieldForm] = useState('');

useStateは引数に初期値を渡すと、ステートフルな値(technologyFieldForm)と、それを更新するための関数(setTechnologyFieldForm)を返します。

Local Stateを更新するための関数は、フォームのonChange属性に渡すハンドラの中などで使います。

src/app/components/Form.tsx
import { InputText } from 'ui/Form';

const handleTechnologyFieldForm = (e: any) => {
  setTechnologyFieldForm(e.target.value);
};

<InputText
  placeholder="Technology field"
  width="375px"
  borderRadius="30px"
  margin="0 0 1rem"
  value={technologyFieldForm}
  onChange={handleTechnologyFieldForm}
/>

こうすることでフォームに入力された内容でtechnologyFieldFormが都度更新されます。

useContext

useContextはGlobal Stateを参照するために使います。
公式ドキュメント - useContext

iCardではStoreでcreateContextによりGlobal Stateを作成しています。
今回はuseContextを扱いたかったため、多少無理やりGlobal Stateを使っていますが、今回のようなアプリであれば、Global Stateを使わずにLocal Stateを使ってpropsによるバケツリレーを行った方がシンプルなコードになると思います。

src/store/index.tsx
type StoreState = {
  profile: ProfileState;
};

const initialState: StoreState = {
  profile: initialProfileState,
};

export const Store = createContext<StoreState | any>(initialState);

export const StoreProvider: React.FC<React.ReactNode> = ({ children }) => {
  const [profileState, profileDispatch] = useReducer(
    profileReducer,
    initialProfileState,
  );

  const state = {
    profile: profileState,
  };

  const dispatch = {
    profile: profileDispatch,
  };

  return (
    <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider>
  );
};
src/index.tsx
import { StoreProvider } from 'store';

<StoreProvider>
  <App />
</StoreProvider>
src/app/components/Display.tsx
import { useContext } from 'react';
import { Store } from 'store';
import { Paragraph } from 'ui/Typography';

const { state } = useContext(Store);

<Paragraph margin="0 0 10px">
  {state.profile.technologyField}
</Paragraph>

src/store/index.tsxでは<Store.Provider />を定義し、src/index.tsxでそれを呼び出すことで、アプリ全体でStoreの値が参照・更新できるようにしています。

そして、src/app/components/Display.tsxでStoreの値を参照しています。
useContextは引数にコンテクストオブジェクトを渡すと、そのコンテクストの現在値を返します。

useReducer

useReducerはuseStateの代替品です。
Reduxを使っている方はおなじみかもしれません。
公式ドキュメント - useReducer

iCardではフォームに入力され、保存ボタンを押されたカードの情報をuseReducerを使ってStoreに保存し、Contextを使って参照しています。
今回はReduxを再現したものを作成してみたかったので、あえてuseReducerを使っていますが、通常、useReducerがuseStateより好ましいのは、複数の値にまたがる複雑なstateロジックがある場合や、前のstateに基づいて次のstateを決める必要がある場合です。(Contextについても同様で今回のようなアプリの構成ならば、Global Stateを使わずにLocal Stateを使った方がシンプルであったかもしれません。)

src/store/reducers.ts
export type ProfileState = {
  technologyField: string;
  technologyStack: string;
};

export type ProfileAction = {
  type: 'SAVE' | 'DELETE';
  state: ProfileState;
};

export const initialProfileState: ProfileState = {
  technologyField: 'Engineer',
  technologyStack: 'Hello World!',
};

export const profileReducer = (
  oldState: ProfileState,
  action: ProfileAction,
) => {
  switch (action.type) {
    case 'SAVE':
      return action.state;
    case 'DELETE':
      return initialProfileState;
    default:
      throw new TypeError(`Illegal type of action: ${action.type}`);
  }
};
src/store/index.tsx
import { useReducer } from 'react';
import { profileReducer, initialProfileState } from 'store/reducers';

const [profileState, profileDispatch] = useReducer(
  profileReducer,
  initialProfileState,
);
src/app/components/Form.tsx
import { useContext } from 'react';
import { Store } from 'store';
import { Button } from 'ui/Button';
import MaterialIcons from 'ui/MaterialIcons';

const { dispatch } = useContext(Store);

const clickSaveButton = () => {
  dispatch.profile({
    type: 'SAVE',
    state: {
      technologyField: technologyFieldForm,
      technologyStack: technologyStackForm,
    },
  });
};

<Button
  width="375px"
  borderRadius="30px"
  color="darkgreen"
  margin="0 0 1rem"
  onClick={clickSaveButton}
  disabled={disabled}
>
  <MaterialIcons
    color="darkgreen"
    fontSize="1rem"
    lineHeight="1.2rem"
    margin="0 10px 0"
  >
    save_alt
  </MaterialIcons>
  Save
</Button>

useReducerは第一引数にreducerを、第二引数に初期値を渡すと、現在のstateをdispatchメソッドとペアにして返します。
iCardではreducerと初期値はsrc/store/reducers.tsで定義し、それらをsrc/store/index.tsxでimportし、useReducerの引数として渡しています。

そして、src/app/components/Form.tsxでStoreの値を更新しています。

useEffect

今回のアプリでは使っていませんが、useEffectについても紹介しておきます。

useEffectはAPI通信のような副作用を有する可能性のある命令型のコードを受け付けます。
クラスコンポーネントのライフサイクルメソッドの代替品です。
公式ドキュメント - useEffect

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

useEffectは第一引数に副作用のある処理を、第二引数に副作用が依存している値の配列を渡します。
第二引数に空配列を渡すと初回レンダリング時のみ実行されます。
この例では、props.sourceが変更された場合にのみ再実行されます。

HooksとContextを使うメリット、デメリット

ここからは業務で実際に使ってみて感じたHooksとContextを使うメリット、デメリットについて書いていきます。

メリット

  • 記述量が少なくて済む
  • 可読性が高く、シンプルなコードが書ける

デメリット

  • Reduxのような非同期の鉄板ライブラリがない(redux-thunkやredux-saga)

総じてシンプルなコードが書けるのがメリットだと思います。

デメリットも設計に気をつけたり、複雑なことをやろうとしすぎたりしなければ問題にならない気がします。

Contextをコンポーネントツリーのデータベースのように扱い、子や孫以下の階層ではそれを単純に参照し、フォームなどで編集するときはフォーム専用のLocal Stateを定義することで切り分けて管理すれば書きやすく、読みやすいコードになるんじゃないかなと思います。

おわりに

HooksとContextを使うと、大体のことがシンプルに書けるので、これからは関数コンポーネントで書くのがReactの鉄板になりそうです。Hooks本当に書きやすい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】作って学ぶ、HooksとContext

はじめに

モダンReactでの開発ではHooksとContextを使うことが多くなってきたと思います。(大規模SPAではReduxが活きるケースも多そうですが。)
特にHooksを用いた関数コンポーネントを使うのはもはや必須で、特別な理由がない限りクラスコンポーネントを使うことはなくなりました。
今回はHooksとContextを使った小さなアプリを作りながら、その使い方とメリット・デメリットについて書いていきたいと思います。

筆者のReact歴

実務で半年、個人開発で使い始めてから1年といったところです。
Hooksがリリースされた頃ですね。
保守でクラスコンポーネントを修正することはありますが、新規開発ではもっぱらHooksを用いて関数コンポーネントを使っています。

実際のアプリとコード

エンジニア向けにツイッターのヘッダー用画像を作成する「iCard」というアプリを作成しました。
React + TypeScriptで作成し、Firebaseにデプロイしています。
サイトURL
GitHubリポジトリ
image.png
「技術分野」と「技術スタック」を入力して、スクリーンショットを撮ることでツイッターのヘッダー用画像を作成することができます。

HooksとContextの概要

Hooks

今までStateを管理したり、ライフサイクルメソッドを使ったりできるのはクラスコンポーネントだけでしたが、それを関数コンポーネントでも可能にしたのがHooksです。(ライフサイクルについては、Hooksではほぼ意識することなく開発できるようになった。)
Hooksを使って関数コンポーネントで開発すると、関連する機能に基づいて、1つのコンポーネントを複数の小さな関数に分割することができ、保守性・可読性の高いコードを書くことができます。
公式ドキュメント - フックの導入

Context

Contextを使うとコンポーネントツリーの中でグローバルにデータを管理することができます。
親コンポーネントで定義したContextを子や孫コンポーネントで簡単に参照できます。
大雑把に言うと簡易版Reduxのようなものです。
このContextがHooksの登場(useContext)によって大幅に使いやすくなっています。
公式ドキュメント - コンテクスト

実例

useState

useStateはLocal Stateを管理するために使います。
公式ドキュメント - useState

iCardではフォームに入力された値の管理に使っています。

src/app/components/Form.tsx
import { useState } from 'react';

const [technologyFieldForm, setTechnologyFieldForm] = useState('');

useStateは引数に初期値を渡すと、ステートフルな値(technologyFieldForm)と、それを更新するための関数(setTechnologyFieldForm)を返します。

Local Stateを更新するための関数は、フォームのonChange属性に渡すハンドラの中などで使います。

src/app/components/Form.tsx
import { InputText } from 'ui/Form';

const handleTechnologyFieldForm = (e: any) => {
  setTechnologyFieldForm(e.target.value);
};

<InputText
  placeholder="Technology field"
  width="375px"
  borderRadius="30px"
  margin="0 0 1rem"
  value={technologyFieldForm}
  onChange={handleTechnologyFieldForm}
/>

こうすることでフォームに入力された内容でtechnologyFieldFormが都度更新されます。

useContext

useContextはGlobal Stateを参照するために使います。
公式ドキュメント - useContext

iCardではStoreでcreateContextによりGlobal Stateを作成しています。
今回はuseContextを扱いたかったため、多少無理やりGlobal Stateを使っていますが、今回のようなアプリであれば、Global Stateを使わずにLocal Stateを使ってpropsによるバケツリレーを行った方がシンプルなコードになると思います。

src/store/index.tsx
type StoreState = {
  profile: ProfileState;
};

const initialState: StoreState = {
  profile: initialProfileState,
};

export const Store = createContext<StoreState | any>(initialState);

export const StoreProvider: React.FC<React.ReactNode> = ({ children }) => {
  const [profileState, profileDispatch] = useReducer(
    profileReducer,
    initialProfileState,
  );

  const state = {
    profile: profileState,
  };

  const dispatch = {
    profile: profileDispatch,
  };

  return (
    <Store.Provider value={{ state, dispatch }}>{children}</Store.Provider>
  );
};
src/index.tsx
import { StoreProvider } from 'store';

<StoreProvider>
  <App />
</StoreProvider>
src/app/components/Display.tsx
import { useContext } from 'react';
import { Store } from 'store';
import { Paragraph } from 'ui/Typography';

const { state } = useContext(Store);

<Paragraph margin="0 0 10px">
  {state.profile.technologyField}
</Paragraph>

src/store/index.tsxでは<Store.Provider />を定義し、src/index.tsxでそれを呼び出すことで、アプリ全体でStoreの値を参照・更新できるようにしています。

そして、src/app/components/Display.tsxでStoreの値を参照しています。
useContextは引数にコンテクストオブジェクトを渡すと、そのコンテクストの現在値を返します。

useReducer

useReducerはuseStateの代替品です。
Reduxを使っている方はおなじみかもしれません。
公式ドキュメント - useReducer

iCardではフォームに入力され、保存ボタンを押されたカードの情報をuseReducerを使ってStoreに保存しています。
今回はReduxを再現したものを作成してみたかったので、あえてuseReducerを使っていますが、通常、useReducerがuseStateより好ましいのは、複数の値にまたがる複雑なstateロジックがある場合や、前のstateに基づいて次のstateを決める必要がある場合です。

src/store/reducers.ts
export type ProfileState = {
  technologyField: string;
  technologyStack: string;
};

export type ProfileAction = {
  type: 'SAVE' | 'DELETE';
  state: ProfileState;
};

export const initialProfileState: ProfileState = {
  technologyField: 'Engineer',
  technologyStack: 'Hello World!',
};

export const profileReducer = (
  oldState: ProfileState,
  action: ProfileAction,
) => {
  switch (action.type) {
    case 'SAVE':
      return action.state;
    case 'DELETE':
      return initialProfileState;
    default:
      throw new TypeError(`Illegal type of action: ${action.type}`);
  }
};
src/store/index.tsx
import { useReducer } from 'react';
import { profileReducer, initialProfileState } from 'store/reducers';

const [profileState, profileDispatch] = useReducer(
  profileReducer,
  initialProfileState,
);
src/app/components/Form.tsx
import { useContext } from 'react';
import { Store } from 'store';
import { Button } from 'ui/Button';
import MaterialIcons from 'ui/MaterialIcons';

const { dispatch } = useContext(Store);

const clickSaveButton = () => {
  dispatch.profile({
    type: 'SAVE',
    state: {
      technologyField: technologyFieldForm,
      technologyStack: technologyStackForm,
    },
  });
};

<Button
  width="375px"
  borderRadius="30px"
  color="darkgreen"
  margin="0 0 1rem"
  onClick={clickSaveButton}
  disabled={disabled}
>
  <MaterialIcons
    color="darkgreen"
    fontSize="1rem"
    lineHeight="1.2rem"
    margin="0 10px 0"
  >
    save_alt
  </MaterialIcons>
  Save
</Button>

useReducerは第一引数にreducerを、第二引数に初期値を渡すと、現在のstateをdispatchメソッドとペアにして返します。
iCardではreducerと初期値はsrc/store/reducers.tsで定義し、それらをsrc/store/index.tsxでimportし、useReducerの引数として渡しています。

そして、src/app/components/Form.tsxでStoreの値を更新しています。

useEffect

今回のアプリでは使っていませんが、useEffectについても紹介しておきます。

useEffectはAPI通信のような副作用を有する可能性のある命令型のコードを受け付けます。
クラスコンポーネントのライフサイクルメソッドの代替品です。
公式ドキュメント - useEffect

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

useEffectは第一引数に副作用のある処理を、第二引数に副作用が依存している値の配列を渡します。
第二引数に空配列を渡すと初回レンダリング時のみ実行されます。
この例では、props.sourceが変更された場合にのみ再実行されます。

HooksとContextを使うメリット、デメリット

ここからは業務で実際に使ってみて感じたHooksとContextを使うメリット、デメリットについて書いていきます。

メリット

  • 記述量が少なくて済む
  • 可読性が高く、シンプルなコードが書ける

デメリット

  • Reduxのような非同期の鉄板ライブラリがない(redux-thunkやredux-saga)

総じてシンプルなコードが書けるのがメリットだと思います。

デメリットも設計に気をつけたり、複雑なことをやろうとしすぎたりしなければ問題にならない気がします。

Contextをコンポーネントツリーのデータベースのように扱い、子や孫以下の階層ではそれを単純に参照し、フォームなどで編集するときはフォーム専用のLocal Stateを定義することで切り分けて管理すれば書きやすく、読みやすいコードになるんじゃないかなと思います。

おわりに

HooksとContextを使うと、大体のことがシンプルに書けるので、これからは関数コンポーネントで書くのがReactの鉄板になりそうです。Hooks本当に書きやすい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

一晩でReact×Laravel(API)×Dockerを利用したWebサービスを作ったTDD(徹夜駆動開発)のお話

はじめに

初めまして、大阪でWebエンジニアをしているかっぽと申します。
今回、主催者の並里氏のお誘いもあり大阪で行われた一泊二日のエンジニア合宿に参加してきたのですが
思っていた以上に刺激的で有意義な時間を過ごせましたので記事にしようと思い書いている次第です。
こちらの記事は合宿についての自分チーム主体の偏見の塊のような個人的共有レポートです。

第1回大阪エンジニア開発合宿(OsakaDevelopmentCamp)とは

2020/02/22(土) 〜 2020/02/23(日) に大阪の難波周辺で行われたエンジニアの開発合宿
Laravelのもくもく会や勉強会、ハンズオンなど多くのエンジニアのイベントを企画しているナミザト氏の今度開発合宿やってみたいな〜というぽっとでのツイートをきっかけに賛同したフットワーク軽めのエンジニア9人が集まり、3人1チームとなって前日から次の日の朝まで眠らずにひとつWebサービスを開発するというエンジニアにとって夢のような合宿です

前置き

今回の開発合宿の参加者

チームサイトウ

チームアマゾネス

チームじゃけぇ

合宿日時

2020/02/22(土) 16:00 〜 2020/02/23(日) 10:00

当日の予定

当日はチームで一つの成果物(web/アプリ問わない)を23日の7時までに作成して、
23日の8時にはみんなの前でそれぞれ成果物を発表する

合宿のルール

  • 技術選定はチームで話し合い決定する
  • 成果物はチームで話し合い決定する
  • 開発終了後の成果物の所有等はチームで話し合い決定する(自由)

今回の合宿、開始日までに環境構築や事前準備はOKですが、実装は開始日の16:00からしか行ってはいけないという暗黙のルールがありました。
しかし、なんと3チーム中2チームが16:00以前から実装を進めていたとの情報が!!(ずるい)

16:30~ 合宿開始

そんなこんなで始まった開発合宿ですが、さすが有名なエンジニアさんが多く集まっているだけあって、始まったとたんからバリバリと実装を開始しました。(私のチーム以外は。)
IMG_0706.JPG

ふんわりとしか役割を決めていなかった私のチーム、チームじゃけぇはその姿を見て呆然とし、

「え、あ、フロントする?サーバする?」
「...どっちやりたい?」
「どっちでも...」

とスタートから挙動不審
幸先の悪いスタートとなりました。

今回チームじゃけぇが開発合宿で開発した成果物と選定した技術スタック

今回チームじゃけぇが開発合宿で開発したのは...

React×Laravel(API)×Dockerを利用したある分野に特化した専用掲示板!!

※正式な公表のタイミングがあると思うので名前はあえて控えます

フロントエンド: React (じゃけぇ担当)
サーバサイド: Laravel API (はる・かっぽ担当)

今回のソースコードのフォルダ構成ですが、ReactとLaravelを同じ階層のリポジトリに置き、開発環境はDockerを利用、本番環境のデプロイ先はフロント・サーバそれぞれのサーバという構成でWebサービスを作りました。(この構成、個人的にすごくやりやすかったです)
余裕があればGitHub共有したいと思います!

Dockerについて

フロントのDockerはcreate-react-appを参考に
サーバのDockerはphp-for-everyoneを参考に
フロント・サーバの各々のDockerの良さを引っ張ってきて、つなぎこんでガッチャンコした
今回特別に作ったDockerを使用しました。
GitHubについては、落ち着いたらリポジトリとは別にReact×Laravel(API)×Dockerの環境構築用としてDocker環境を共有する予定です。
Facebookの皆さん、Laravel-shibuyaの皆さん 参考にさせていただきました。ありがとうございます。)

本番サーバについて

ReactはNow
LaravelはHerku
のサーバを利用しました。
Laravelは完全にAPIとし、DBの値やビジネスロジックでゴニョゴニョした値をJSON型で返す処理のみで、データ表示はReactにすべてお任せました。
デプロイは、masterブランチにmergeされたと同時に自動で本番反映されるように設定致しました。

17:00~ チームじゃけぇ始動

それぞれの役割も決まり、チームじゃけぇもいざ開発スタートです!

当日までに、はるさんがWebサービスのデザインをFigmaで作成していただいていたのと、じゃけぇさんの技術力でフロントは初速充分でバリバリ進みます。

肝心のサーバサイドはというと...

私自身、普段PHPのSymfonyを使用しており、Laravelの業務経験はありましたが約一年ぶりに触ります。
はるさんも普段はJavaを使った開発が多いとのことで

「ヤベェ、どうします?w」

「まずはAPIの設定から?」

とあたふた。

しかし、人間の記憶とは素晴らしいもので、一度触り始めると1年前のLaravelの記憶がどんどん蘇ってきました。
まさに、「書けるっ!書けるぞっ!!」状態

はるさんも吸収力がナプキンレベル、1つ進めると後は自走状態に。

そこからは3人とも、時間を忘れるように開発に没頭しました。

20:00~ 晩御飯

今回の合宿の晩御飯は豪華!
宅配のピザLsize3枚(チーズ・照り焼き・マヨポテト)
寿司を5人前(サーモンセット3人前・マグロセット2人前)
お寿司の注文にはUberEatsを利用しました。

20:00過ぎに全ての注文が届き、支払いも済ませたので食事にしましょうと皆さん一休み。
いざ食べようと広げてみると...

「あれ?マグロセット2人前なくね?」

どういうことか、支払いはお寿司5人前分したのに注文したマグロセット2人前が配達されませんでした。
なにしとんねんUberEats
IMG_0702.JPG

21:00~ 再び事件

ご飯も食べ終えたので開発する人部屋に残り、銭湯行く人は行きましょうということになりました。
私とはるさんは再び開発を進め、じゃけぇさんは銭湯に。

ここでまたまた事件発生

なんと、突然フロントが表示されない!!!

しかも、フロント担当のじゃけぇさんが銭湯に行ってしまっていません。

ドヤ顔で
「yarn install したら治るでしょ」 と高を括っていました。

が、インストールしても

「あれ?治らない、、、」

結局、はるさんと1時間ほど格闘しました。

じゃけぇさんが帰ってきて解決しましたが、
結論、yarn install の場所を間違えていました。

しょうもないことで時間取られてしまう エンジニアあるあるですよね。

これにより、フロント頼りの開発を辞め、API開発時に便利なGoogleChromeの拡張ツールTalend Api Tester
を使いこなせるようになりました。(いや、使ってなかったんかい!)

22:00~6:00 空白の8時間

ふと時計を見るともう6時。

ほとんど記憶がございません。
意識朦朧の中、必死で実装しまくってました。

結果的には、
フロント側は基本的なフォーム作成やデザイン実装から、ストレージを利用したデータ保存機能まで。
サーバ側はAPIでのRouting・基本的なCRUD処理からLaravelのEloquentを利用したソート切り替え処理まで。

難なくクリアし、どちらともキリの良いところで実装を終えられました。万歳。

8:00~ 成果物発表

やはり、一晩で3人で1つのWebサービスを作り上げるのは至難の業。
結論を言うと、3チームとも時間の問題もあり、完成に至らずでした。
しかし、全員が触ったことのない新しい技術に挑戦するといった目標を掲げ最後まで諦めずに頑張るチーム
Laravelの認証周りの理解に苦しみながらも様々な記事から豊富な知識を蓄えたチーム等、
どのチームも開発に没頭し、誰一人脱落することなく開発合宿を終えることができました!

成果物発表が終わったあとは眠さの限界でチェックアウトの時間まで床で爆睡。
私一人だけ、昨日の残りのピザの残飯処理をすると言うカオスな光景となりました。
IMG_0704.JPG

まとめ

以上、今回の大阪エンジニア開発合宿(OsakaDevelopmentCamp)の個人的レポートでした。
個人的には、様々なトラブルを乗り越えぶっ通しで久々にLaravel触れることができて大変嬉しかったです!
貴重な体験をさせていただきまして、主催者の並里氏をはじめ参加された皆様、ありがとうございました!
そしてお疲れ様でした!
次回開催はいつになるかわかりませんが大いに楽しみですね?
IMG_0709.JPG
(疲れ切った表情の皆さんを載せるのは心が痛かったのでマンウィズっぽく)

有志ですがプロダクト参加者募集してます

今回実装した掲示板Webサービスですが React×Laravel(API)×Docker で環境構築されており、プロダクト自体も初学者に大変わかりやすく勉強になるプロダクトだと自負しております。
3人とも完全に趣味の範囲でやっておりますので、お給料等をお支払いすることはできませんが、もしこのプロダクトに参加して一緒にLaravel・Reactの勉強をしたり自分のポートフォリオの一つとして利用したいというエンジニアの方(実務未経験でも可)いらっしゃいましたら気軽にDM・リプでご連絡くださいませ〜
一緒に楽しくWebサービスを作りましょう?

最後までお読みいただきありがとうございました〜?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

開発合宿でReact×Laravel(API)×Dockerを利用したWebサービスを作ったお話

はじめに

初めまして、大阪でWebエンジニアをしているかっぽと申します。
今回、主催者の並里氏のお誘いもあり大阪で行われた一泊二日のエンジニア合宿に参加してきたのですが
思っていた以上に刺激的で有意義な時間を過ごせましたので記事にしようと思い書いている次第です。
こちらの記事は合宿についての自分チーム主体の偏見の塊のような個人的共有レポートです。

第1回大阪エンジニア開発合宿(OsakaDevelopmentCamp)とは

2020/02/22(土) 〜 2020/02/23(日) に大阪の難波周辺で行われたエンジニアの開発合宿
Laravelのもくもく会や勉強会、ハンズオンなど多くのエンジニアのイベントを企画しているナミザト氏の今度開発合宿やってみたいな〜というぽっとでのツイートをきっかけに賛同したフットワーク軽めのエンジニア9人が集まり、3人1チームとなって前日から次の日の朝まで眠らずにひとつWebサービスを開発するというエンジニアにとって夢のような合宿です

前置き

今回の開発合宿の参加者

チームサイトウ

チームアマゾネス

チームじゃけぇ

合宿日時

2020/02/22(土) 16:00 〜 2020/02/23(日) 10:00

当日の予定

当日はチームで一つの成果物(web/アプリ問わない)を23日の7時までに作成して、
23日の8時にはみんなの前でそれぞれ成果物を発表する

合宿のルール

  • 技術選定はチームで話し合い決定する
  • 成果物はチームで話し合い決定する
  • 開発終了後の成果物の所有等はチームで話し合い決定する(自由)

今回の合宿、開始日までに環境構築や事前準備はOKですが、実装は開始日の16:00からしか行ってはいけないという暗黙のルールがありました。
しかし、なんと3チーム中2チームが16:00以前から実装を進めていたとの情報が!!(ずるい)

16:30~ 合宿開始

そんなこんなで始まった開発合宿ですが、さすが有名なエンジニアさんが多く集まっているだけあって、始まったとたんからバリバリと実装を開始しました。(私のチーム以外は。)
IMG_0706.JPG

ふんわりとしか役割を決めていなかった私のチーム、チームじゃけぇはその姿を見て呆然とし、

「え、あ、フロントする?サーバする?」
「...どっちやりたい?」
「どっちでも...」

とスタートから挙動不審
幸先の悪いスタートとなりました。

今回チームじゃけぇが開発合宿で開発した成果物と選定した技術スタック

今回チームじゃけぇが開発合宿で開発したのは...

React×Laravel(API)×Dockerを利用したある分野に特化した専用掲示板!!

※正式な公表のタイミングがあると思うので名前はあえて控えます

フロントエンド: React (じゃけぇ担当)
サーバサイド: Laravel API (はる・かっぽ担当)

今回のソースコードのフォルダ構成ですが、ReactとLaravelを同じ階層のリポジトリに置き、開発環境はDockerを利用、本番環境のデプロイ先はフロント・サーバそれぞれのサーバという構成でWebサービスを作りました。(この構成、個人的にすごくやりやすかったです)
余裕があればGitHub共有したいと思います!

Dockerについて

フロントのDockerはcreate-react-appを参考に
サーバのDockerはphp-for-everyoneを参考に
フロント・サーバの各々のDockerの良さを引っ張ってきて、つなぎこんでガッチャンコした
今回特別に作ったDockerを使用しました。
GitHubについては、落ち着いたらリポジトリとは別にReact×Laravel(API)×Dockerの環境構築用としてDocker環境を共有する予定です。
Facebookの皆さん、Laravel-shibuyaの皆さん 参考にさせていただきました。ありがとうございます。)

本番サーバについて

ReactはNow
LaravelはHerku
のサーバを利用しました。
Laravelは完全にAPIとし、DBの値やビジネスロジックでゴニョゴニョした値をJSON型で返す処理のみで、データ表示はReactにすべてお任せました。
デプロイは、masterブランチにmergeされたと同時に自動で本番反映されるように設定致しました。

17:00~ チームじゃけぇ始動

それぞれの役割も決まり、チームじゃけぇもいざ開発スタートです!

当日までに、はるさんがWebサービスのデザインをFigmaで作成していただいていたのと、じゃけぇさんの技術力でフロントは初速充分でバリバリ進みます。

肝心のサーバサイドはというと...

私自身、普段PHPのSymfonyを使用しており、Laravelの業務経験はありましたが約一年ぶりに触ります。
はるさんも普段はJavaを使った開発が多いとのことで

「ヤベェ、どうします?w」

「まずはAPIの設定から?」

とあたふた。

しかし、人間の記憶とは素晴らしいもので、一度触り始めると1年前のLaravelの記憶がどんどん蘇ってきました。
まさに、「書けるっ!書けるぞっ!!」状態

はるさんも吸収力がナプキンレベル、1つ進めると後は自走状態に。

そこからは3人とも、時間を忘れるように開発に没頭しました。

20:00~ 晩御飯

今回の合宿の晩御飯は豪華!
宅配のピザLsize3枚(チーズ・照り焼き・マヨポテト)
寿司を5人前(サーモンセット3人前・マグロセット2人前)
お寿司の注文にはUberEatsを利用しました。

20:00過ぎに全ての注文が届き、支払いも済ませたので食事にしましょうと皆さん一休み。
いざ食べようと広げてみると...

「あれ?マグロセット2人前なくね?」

どういうことか、支払いはお寿司5人前分したのに注文したマグロセット2人前が配達されませんでした。
なにしとんねんUberEats
IMG_0702.JPG

21:00~ 再び事件

ご飯も食べ終えたので開発する人部屋に残り、銭湯行く人は行きましょうということになりました。
私とはるさんは再び開発を進め、じゃけぇさんは銭湯に。

ここでまたまた事件発生

なんと、突然フロントが表示されない!!!

しかも、フロント担当のじゃけぇさんが銭湯に行ってしまっていません。

ドヤ顔で
「yarn install したら治るでしょ」 と高を括っていました。

が、インストールしても

「あれ?治らない、、、」

結局、はるさんと1時間ほど格闘しました。

じゃけぇさんが帰ってきて解決しましたが、
結論、yarn install の場所を間違えていました。

しょうもないことで時間取られてしまう エンジニアあるあるですよね。

これにより、フロント頼りの開発を辞め、API開発時に便利なGoogleChromeの拡張ツールTalend Api Tester
を使いこなせるようになりました。(いや、使ってなかったんかい!)

22:00~6:00 空白の8時間

ふと時計を見るともう6時。

ほとんど記憶がございません。
意識朦朧の中、必死で実装しまくってました。

結果的には、
フロント側は基本的なフォーム作成やデザイン実装から、ストレージを利用したデータ保存機能まで。
サーバ側はAPIでのRouting・基本的なCRUD処理からLaravelのEloquentを利用したソート切り替え処理まで。

難なくクリアし、どちらともキリの良いところで実装を終えられました。万歳。

8:00~ 成果物発表

やはり、一晩で3人で1つのWebサービスを作り上げるのは至難の業。
結論を言うと、3チームとも時間の問題もあり、完成に至らずでした。
しかし、全員が触ったことのない新しい技術に挑戦するといった目標を掲げ最後まで諦めずに頑張るチーム
Laravelの認証周りの理解に苦しみながらも様々な記事から豊富な知識を蓄えたチーム等、
どのチームも開発に没頭し、誰一人脱落することなく開発合宿を終えることができました!

成果物発表が終わったあとは眠さの限界でチェックアウトの時間まで床で爆睡。
私一人だけ、昨日の残りのピザの残飯処理をすると言うカオスな光景となりました。
IMG_0704.JPG

まとめ

以上、今回の大阪エンジニア開発合宿(OsakaDevelopmentCamp)の個人的レポートでした。
個人的には、様々なトラブルを乗り越えぶっ通しで久々にLaravel触れることができて大変嬉しかったです!
貴重な体験をさせていただきまして、主催者の並里氏をはじめ参加された皆様、ありがとうございました!
そしてお疲れ様でした!
次回開催はいつになるかわかりませんが大いに楽しみですね?
IMG_0709.JPG
(疲れ切った表情の皆さんを載せるのは心が痛かったのでマンウィズっぽく)

有志ですがプロダクト参加者募集してます

今回実装した掲示板Webサービスですが React×Laravel(API)×Docker で環境構築されており、プロダクト自体も初学者に大変わかりやすく勉強になるプロダクトだと自負しております。
3人とも完全に趣味の範囲でやっておりますので、お給料等をお支払いすることはできませんが、もしこのプロダクトに参加して一緒にLaravel・Reactの勉強をしたり自分のポートフォリオの一つとして利用したいというエンジニアの方(実務未経験でも可)いらっしゃいましたら気軽にDM・リプでご連絡くださいませ〜
一緒に楽しくWebサービスを作りましょう?

最後までお読みいただきありがとうございました〜?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Redux Toolkit v1.3.0-alpha.x メモ

Redux Toolkit v1.3.0ではcreateAsyncThunkEntityAdapterが追加されます。
https://github.com/reduxjs/redux-toolkit/pull/374

さっそく試してみましょう。

サンプル

https://codesandbox.io/s/ionic-react-redux-toolkit-sample-ous9u
sample

createAsyncThunk

非同期に対応したAction Creatorです。
pendingfulfilledrejectedの3つのアクションを生成してくれます。

src/store/todo/actions/todo.action.ts
import { createAsyncThunk } from '@reduxjs/toolkit';

import { todoService } from '../../../services';

export const fetchAllTodos = createAsyncThunk(
  'todos/fetchAll',
  async (args: { offset?: number; limit?: number }) => {
    const { offset, limit } = args;
    const result = await todoService.fetchAll(offset, limit);
    return { todos: result };
  }
);

export const fetchTodo = createAsyncThunk(
  'todos/fetch',
  async (args: { id: string }) => {
    const { id } = args;
    const result = await todoService.fetch(id);
    return { todo: result };
  }
);

createAsyncThunkが生成するアクションはFSA準拠になっていて、pendingfulfilledrejectedでそれぞれ型が違うようです。何と余計なことを。

interface PendingAction<ThunkArg> {
  type: string;
  meta: {
    requestId: string;
    arg: ThunkArg; // ここにActionの引数が入る
  };
}

interface FulfilledAction<ThunkArg, T> {
  type: string;
  payload: T; // fulfilled時のみ値が入る
  meta: {
    requestId: string;
    arg: ThunkArg;
  };
}

interface RejectedAction<ThunkArg> {
  type: string;
  error: {
    name?: string;
    message?: string; // new Error('hoge')の'hoge'が入る
    code?: string;
    stack?: string;
  } | any;
  meta: {
    requestId: string;
    arg: ThunkArg;
    aborted: boolean;
  };
}

Container Componentで使う場合はこんな感じです。

src/pages/todo/todo-list/containers/todo-list.container.tsx
import { unwrapResult } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';

import { fetchAllTodos } from '../../../../store/todo';

type Props = {
  offset: number;
  limit: number;
};

export const TodoListContainer: React.FC<Props> = props => {
  const { offset, limit } = props;
  const dispatch = useDispatch();

  React.useEffect(() => {
    dispatch(fetchAllTodos({ offset, limit }))  // ActionをDispatch
      .then(unwrapResult)
      .then(payload => {
        console.log({ payload });
      })
      .catch(error => {
        console.log({ error });
      });
  }, [dispatch]);

  return <>...</>
};

unwrapResultを使うとthencatchに分けてくれます。

ThunkActionを受け取ったdispatchがPromiseを返さないのでそのままでは使えず↓のパッチが必要です。

import { ThunkAction } from 'redux-thunk';

// Dispatch overload for redux-thunk
// https://github.com/reduxjs/redux-thunk/pull/278
declare module 'redux' {
  /*
   * Overload to add thunk support to Redux's dispatch() function.
   * Useful for react-redux or any other library which could use this type.
   */
  export interface Dispatch<A extends Action<any> = AnyAction> {
    <TReturnType, TState, TExtraThunkArg>(
      thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, A>
    ): TReturnType;
  }
}

redux-thunkのmasterにマージされていますが、まだリリースされていないようです。

EntityAdapter

@ngrx/entityとほぼ同じです。

Entity

エンティティは以下のようなIDとデータのマップです。

export type EntityId = number | string;
export interface EntityState<T> {
  ids: EntityId[];
  entities: {
    [id: EntityId]: T
  };
}

State

createEntityAdapterの使い方も@ngrx/entityとほぼ一緒です。

src/store/todo/states/todo.state.ts
import { EntityState, createEntityAdapter } from '@reduxjs/toolkit';

import { Todo } from '../../../models';

export interface TodoState extends EntityState<Todo> {
  isFetching: boolean;
  selectedId: string | null;
}

export const adapter = createEntityAdapter<Todo>();

export const initialState: TodoState = adapter.getInitialState({
  isFetching: false,
  selectedId: null
});

Selector

EntityAdapterのgetSelectors()でエンティティ用のセレクタを取得します。
Redux ToolkitのcreateSelectorはReselectのものと同じで自動的にメモ化されます。

src/store/todo/selectors/todo.selector.ts
import { createSelector } from '@reduxjs/toolkit';

import { TodoState, adapter } from '../states';

const { selectAll, selectEntities } = adapter.getSelectors();

const featureStateSelector = (state: { todos: TodoState }) => state.todos;

const entitiesSelector = createSelector(
  featureStateSelector,
  selectEntities
);

export const isFetchingSelector = createSelector(
  featureStateSelector,
  state => state.isFetching
);

export const selectedIdSelector = createSelector(
  featureStateSelector,
  state => state.selectedId
);

export const todosSelector = createSelector(
  featureStateSelector,
  selectAll
);

export const todoSelector = createSelector(
  entitiesSelector,
  selectedIdSelector,
  (entities, id) => (id ? entities[id] || null : null)
);

NgRxのcreateFeatureSelectorも欲しいところ。

Reducer

ActionReducerMapBuilderで書く方のcreateReducerを使いましょう。型推論が利きます。

エンティティの操作はEntityAdapterのヘルパーを使います(ドキュメント)。

  • setAll
  • upsertOne
  • updateOne
  • removeOne

※ NgRxと違ってEntityAdapterのメソッドの引数の順番が逆になっている点に注意

src/store/todo/reducers/todo.reducer.ts
import { createReducer } from '@reduxjs/toolkit';

import { initialState, adapter } from '../states';
import { fetchAllTodos, fetchTodo } from '../actions';

export const reducer = createReducer(initialState, builder =>
  builder
    .addCase(fetchAllTodos.pending, state => {
      return { ...state, isFetching: true };
    })
    .addCase(fetchAllTodos.fulfilled, (state, action) => {
      const { todos } = action.payload;
      return adapter.setAll({ ...state, isFetching: false }, todos);
    })
    .addCase(fetchAllTodos.rejected, state => {
      return { ...state, isFetching: false };
    })
    .addCase(fetchTodo.pending, (state, action) => {
      const { id } = action.meta.arg;
      return { ...state, isFetching: true, selectedId: id };
    })
    .addCase(fetchTodo.fulfilled, (state, action) => {
      const { todo } = action.payload;
      return adapter.upsertOne({ ...state, isFetching: false }, todo);
    })
    .addCase(fetchTodo.rejected, state => {
      return { ...state, isFetching: false };
    })
);

Immerが入ってるのでstateに直接代入できますが、Reducer内だけ書き方が変わるのは一貫性が崩れるので個人的には非推奨です。(:thinking: 治安の悪いチームには必要かも?)

State

特に変わらず。
キー名(todos等)はSelectorと合わせる必要があるので変数で持った方が良かったかもしれません。

src/store/index.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';

import * as todo from './todo';

const reducer = combineReducers({
  todos: todo.reducer
});

export const store = configureStore({ reducer });

ActionのPayloadにDate型が入るなどシリアライズ不可な場合は↓のようにチェックを外します。

const middleware = getDefaultMiddleware({ serializableCheck: false });
export const store = configureStore({
  reducer,
  middleware
});

Slice

今回は使いませんでした。
(ducksパターンのごちゃ混ぜ具合が怖い:fearful:

使う場合はコード量だけでなく型推論が効くか、テストをどう書くか等を調べる必要がありそうです。

まとめ

createAsyncThunkEntityAdapterは良さそうでした。

  • 公式提供の安心感
  • 型による保守性
  • 実装で迷わないオピニオン

去年ThunkActionの型解決で苦労したので、今年はRedux Toolkitを推していきたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Redux Toolkit v1.3.0(Alpha版)メモ

Redux Toolkit v1.3.0ではcreateAsyncThunkEntityAdapterが追加されるようです。
https://github.com/reduxjs/redux-toolkit/pull/374

さっそく試してみましょう。

サンプル

https://codesandbox.io/s/ionic-react-redux-toolkit-sample-ous9u
sample

createAsyncThunk

非同期に対応したAction Creatorです。
pendingfulfilledrejectedの3つのアクションを生成してくれます。

src/store/todo/actions/todo.action.ts
import { createAsyncThunk } from '@reduxjs/toolkit';

import { todoService } from '../../../services';

export const fetchAllTodos = createAsyncThunk(
  'todos/fetchAll',
  async (args: { offset?: number; limit?: number }) => {
    const { offset, limit } = args;
    const result = await todoService.fetchAll(offset, limit);
    return { todos: result };
  }
);

export const fetchTodo = createAsyncThunk(
  'todos/fetch',
  async (args: { id: string }) => {
    const { id } = args;
    const result = await todoService.fetch(id);
    return { todo: result };
  }
);

createAsyncThunkが生成するアクションはFSA準拠になっていて、pendingfulfilledrejectedでそれぞれ型が違うようです。何と余計なことを。

interface PendingAction<ThunkArg> {
  type: string;
  meta: {
    requestId: string;
    arg: ThunkArg; // ここにActionの引数が入る
  };
}

interface FulfilledAction<ThunkArg, T> {
  type: string;
  payload: T; // fulfilled時のみ値が入る
  meta: {
    requestId: string;
    arg: ThunkArg;
  };
}

interface RejectedAction<ThunkArg> {
  type: string;
  error: {
    name?: string;
    message?: string; // new Error('hoge')の'hoge'が入る
    code?: string;
    stack?: string;
  } | any;
  meta: {
    requestId: string;
    arg: ThunkArg;
    aborted: boolean;
  };
}

Container Componentで使う場合はこんな感じです。

src/pages/todo/todo-list/containers/todo-list.container.tsx
import { unwrapResult } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';

import { fetchAllTodos } from '../../../../store/todo';

type Props = {
  offset: number;
  limit: number;
};

export const TodoListContainer: React.FC<Props> = props => {
  const { offset, limit } = props;
  const dispatch = useDispatch();

  React.useEffect(() => {
    dispatch(fetchAllTodos({ offset, limit }))  // ActionをDispatch
      .then(unwrapResult)
      .then(payload => {
        console.log({ payload });
      })
      .catch(error => {
        console.log({ error });
      });
  }, [dispatch]);

  return <>...</>
};

unwrapResultを使うとthencatchに分けてくれます。

ThunkActionを受け取ったdispatchがPromiseを返さないのでそのままでは使えず↓のパッチが必要です。

import { ThunkAction } from 'redux-thunk';

// Dispatch overload for redux-thunk
// https://github.com/reduxjs/redux-thunk/pull/278
declare module 'redux' {
  /*
   * Overload to add thunk support to Redux's dispatch() function.
   * Useful for react-redux or any other library which could use this type.
   */
  export interface Dispatch<A extends Action<any> = AnyAction> {
    <TReturnType, TState, TExtraThunkArg>(
      thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, A>
    ): TReturnType;
  }
}

redux-thunkのmasterにマージされていますが、まだリリースされていないようです。

EntityAdapter

@ngrx/entityとほぼ同じです。

Entity

エンティティは以下のようなIDとデータのマップです。

export type EntityId = number | string;
export interface EntityState<T> {
  ids: EntityId[];
  entities: {
    [id: EntityId]: T
  };
}

State

createEntityAdapterの使い方も@ngrx/entityとほぼ一緒です。

src/store/todo/states/todo.state.ts
import { EntityState, createEntityAdapter } from '@reduxjs/toolkit';

import { Todo } from '../../../models';

export interface TodoState extends EntityState<Todo> {
  isFetching: boolean;
  selectedId: string | null;
}

export const adapter = createEntityAdapter<Todo>();

export const initialState: TodoState = adapter.getInitialState({
  isFetching: false,
  selectedId: null
});

Selector

EntityAdapterのgetSelectors()でエンティティ用のセレクタを取得します。
Redux ToolkitのcreateSelectorはReselectのものと同じで自動的にメモ化されます。

src/store/todo/selectors/todo.selector.ts
import { createSelector } from '@reduxjs/toolkit';

import { TodoState, adapter } from '../states';

const { selectAll, selectEntities } = adapter.getSelectors();

const featureStateSelector = (state: { todos: TodoState }) => state.todos;

const entitiesSelector = createSelector(
  featureStateSelector,
  selectEntities
);

export const isFetchingSelector = createSelector(
  featureStateSelector,
  state => state.isFetching
);

export const selectedIdSelector = createSelector(
  featureStateSelector,
  state => state.selectedId
);

export const todosSelector = createSelector(
  featureStateSelector,
  selectAll
);

export const todoSelector = createSelector(
  entitiesSelector,
  selectedIdSelector,
  (entities, id) => (id ? entities[id] || null : null)
);

NgRxのcreateFeatureSelectorも欲しいところ。

Reducer

ActionReducerMapBuilderで書く方のcreateReducerを使いましょう。型推論が利きます。

エンティティの操作はEntityAdapterのヘルパーを使います(ドキュメント)。

  • setAll
  • upsertOne
  • updateOne
  • removeOne

※ NgRxと違ってEntityAdapterのメソッドの引数の順番が逆になっている点に注意

src/store/todo/reducers/todo.reducer.ts
import { createReducer } from '@reduxjs/toolkit';

import { initialState, adapter } from '../states';
import { fetchAllTodos, fetchTodo } from '../actions';

export const reducer = createReducer(initialState, builder =>
  builder
    .addCase(fetchAllTodos.pending, state => {
      return { ...state, isFetching: true };
    })
    .addCase(fetchAllTodos.fulfilled, (state, action) => {
      const { todos } = action.payload;
      return adapter.setAll({ ...state, isFetching: false }, todos);
    })
    .addCase(fetchAllTodos.rejected, state => {
      return { ...state, isFetching: false };
    })
    .addCase(fetchTodo.pending, (state, action) => {
      const { id } = action.meta.arg;
      return { ...state, isFetching: true, selectedId: id };
    })
    .addCase(fetchTodo.fulfilled, (state, action) => {
      const { todo } = action.payload;
      return adapter.upsertOne({ ...state, isFetching: false }, todo);
    })
    .addCase(fetchTodo.rejected, state => {
      return { ...state, isFetching: false };
    })
);

Immerが入ってるのでstateに直接代入できますが、Reducer内だけ書き方が変わるのは一貫性が崩れるので個人的には非推奨です。(:thinking: 治安の悪いチームには必要かも?)

State

特に変わらず。
キー名(todos等)はSelectorと合わせる必要があるので変数で持った方が良かったかもしれません。

src/store/index.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';

import * as todo from './todo';

const reducer = combineReducers({
  todos: todo.reducer
});

export const store = configureStore({ reducer });

ActionのPayloadにDate型が入るなどシリアライズ不可な場合は↓のようにチェックを外します。

const middleware = getDefaultMiddleware({ serializableCheck: false });
export const store = configureStore({
  reducer,
  middleware
});

Slice

今回は使いませんでした。
(ducksパターンのごちゃ混ぜ具合が怖い:fearful:

使う場合はコード量だけでなく型推論が効くか、テストをどう書くか等を調べる必要がありそうです。

まとめ

createAsyncThunkEntityAdapterは良さそうでした。

  • 公式提供の安心感
  • 型による保守性
  • 実装で迷わないオピニオン

去年ThunkActionの型解決で苦労したので、今年はRedux Toolkitを推していきたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Redux Toolkit v1.3.0(Alpha版)の新機能を使ってみよう

Redux Toolkit v1.3.0ではcreateAsyncThunkEntityAdapterが追加されるようです。
https://github.com/reduxjs/redux-toolkit/pull/374

さっそく試してみましょう。

サンプル

https://codesandbox.io/s/ionic-react-redux-toolkit-sample-ous9u
sample

createAsyncThunk

非同期に対応したAction Creatorです。
pendingfulfilledrejectedの3つのアクションを生成してくれます。

src/store/todo/actions/todo.action.ts
import { createAsyncThunk } from '@reduxjs/toolkit';

import { todoService } from '../../../services';

export const fetchAllTodos = createAsyncThunk(
  'todos/fetchAll',
  async (args: { offset?: number; limit?: number }) => {
    const { offset, limit } = args;
    const result = await todoService.fetchAll(offset, limit);
    return { todos: result };
  }
);

export const fetchTodo = createAsyncThunk(
  'todos/fetch',
  async (args: { id: string }) => {
    const { id } = args;
    const result = await todoService.fetch(id);
    return { todo: result };
  }
);

createAsyncThunkが生成するアクションはFSA準拠になっていて、pendingfulfilledrejectedでそれぞれ型が違うようです。何と余計なことを。

interface PendingAction<ThunkArg> {
  type: string;
  meta: {
    requestId: string;
    arg: ThunkArg; // ここにActionの引数が入る
  };
}

interface FulfilledAction<ThunkArg, T> {
  type: string;
  payload: T; // fulfilled時のみ値が入る
  meta: {
    requestId: string;
    arg: ThunkArg;
  };
}

interface RejectedAction<ThunkArg> {
  type: string;
  error: {
    name?: string;
    message?: string; // new Error('hoge')の'hoge'が入る
    code?: string;
    stack?: string;
  } | any;
  meta: {
    requestId: string;
    arg: ThunkArg;
    aborted: boolean;
  };
}

Container Componentで使う場合はこんな感じです。

src/pages/todo/todo-list/containers/todo-list.container.tsx
import { unwrapResult } from '@reduxjs/toolkit';
import { useDispatch } from 'react-redux';

import { fetchAllTodos } from '../../../../store/todo';

type Props = {
  offset: number;
  limit: number;
};

export const TodoListContainer: React.FC<Props> = props => {
  const { offset, limit } = props;
  const dispatch = useDispatch();

  React.useEffect(() => {
    dispatch(fetchAllTodos({ offset, limit }))  // ActionをDispatch
      .then(unwrapResult)
      .then(payload => {
        console.log({ payload });
      })
      .catch(error => {
        console.log({ error });
      });
  }, [dispatch]);

  return <>...</>
};

unwrapResultを使うとthencatchに分けてくれます。

ThunkActionを受け取ったdispatchがPromiseを返さないのでそのままでは使えず↓のパッチが必要です。

import { ThunkAction } from 'redux-thunk';

// Dispatch overload for redux-thunk
// https://github.com/reduxjs/redux-thunk/pull/278
declare module 'redux' {
  /*
   * Overload to add thunk support to Redux's dispatch() function.
   * Useful for react-redux or any other library which could use this type.
   */
  export interface Dispatch<A extends Action<any> = AnyAction> {
    <TReturnType, TState, TExtraThunkArg>(
      thunkAction: ThunkAction<TReturnType, TState, TExtraThunkArg, A>
    ): TReturnType;
  }
}

redux-thunkのmasterにマージされていますが、まだリリースされていないようです。

EntityAdapter

@ngrx/entityとほぼ同じです。

Entity

エンティティは以下のようなIDとデータのマップです。

export type EntityId = number | string;
export interface EntityState<T> {
  ids: EntityId[];
  entities: {
    [id: EntityId]: T
  };
}

State

createEntityAdapterの使い方も@ngrx/entityとほぼ一緒です。

src/store/todo/states/todo.state.ts
import { EntityState, createEntityAdapter } from '@reduxjs/toolkit';

import { Todo } from '../../../models';

export interface TodoState extends EntityState<Todo> {
  isFetching: boolean;
  selectedId: string | null;
}

export const adapter = createEntityAdapter<Todo>();

export const initialState: TodoState = adapter.getInitialState({
  isFetching: false,
  selectedId: null
});

Selector

EntityAdapterのgetSelectors()でエンティティ用のセレクタを取得します。
Redux ToolkitのcreateSelectorはReselectのものと同じで自動的にメモ化されます。

src/store/todo/selectors/todo.selector.ts
import { createSelector } from '@reduxjs/toolkit';

import { TodoState, adapter } from '../states';

const { selectAll, selectEntities } = adapter.getSelectors();

const featureStateSelector = (state: { todos: TodoState }) => state.todos;

const entitiesSelector = createSelector(
  featureStateSelector,
  selectEntities
);

export const isFetchingSelector = createSelector(
  featureStateSelector,
  state => state.isFetching
);

export const selectedIdSelector = createSelector(
  featureStateSelector,
  state => state.selectedId
);

export const todosSelector = createSelector(
  featureStateSelector,
  selectAll
);

export const todoSelector = createSelector(
  entitiesSelector,
  selectedIdSelector,
  (entities, id) => (id ? entities[id] || null : null)
);

NgRxのcreateFeatureSelectorも欲しいところ。

Reducer

ActionReducerMapBuilderで書く方のcreateReducerを使いましょう。型推論が利きます。

エンティティの操作はEntityAdapterのヘルパーを使います(ドキュメント)。

  • setAll
  • upsertOne
  • updateOne
  • removeOne

※ NgRxと違ってEntityAdapterのメソッドの引数の順番が逆になっている点に注意

src/store/todo/reducers/todo.reducer.ts
import { createReducer } from '@reduxjs/toolkit';

import { initialState, adapter } from '../states';
import { fetchAllTodos, fetchTodo } from '../actions';

export const reducer = createReducer(initialState, builder =>
  builder
    .addCase(fetchAllTodos.pending, state => {
      return { ...state, isFetching: true };
    })
    .addCase(fetchAllTodos.fulfilled, (state, action) => {
      const { todos } = action.payload;
      return adapter.setAll({ ...state, isFetching: false }, todos);
    })
    .addCase(fetchAllTodos.rejected, state => {
      return { ...state, isFetching: false };
    })
    .addCase(fetchTodo.pending, (state, action) => {
      const { id } = action.meta.arg;
      return { ...state, isFetching: true, selectedId: id };
    })
    .addCase(fetchTodo.fulfilled, (state, action) => {
      const { todo } = action.payload;
      return adapter.upsertOne({ ...state, isFetching: false }, todo);
    })
    .addCase(fetchTodo.rejected, state => {
      return { ...state, isFetching: false };
    })
);

Immerが入ってるのでstateに直接代入できますが、Reducer内だけ書き方が変わるのは一貫性が崩れるので個人的には非推奨です。(:thinking: 治安の悪いチームには必要かも?)

State

特に変わらず。
キー名(todos等)はSelectorと合わせる必要があるので変数で持った方が良かったかもしれません。

src/store/index.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';

import * as todo from './todo';

const reducer = combineReducers({
  todos: todo.reducer
});

export const store = configureStore({ reducer });

ActionのPayloadにDate型が入るなどシリアライズ不可な場合は↓のようにチェックを外します。

const middleware = getDefaultMiddleware({ serializableCheck: false });
export const store = configureStore({
  reducer,
  middleware
});

Slice

今回は使いませんでした。
(ducksパターンのごちゃ混ぜ具合が怖い:fearful:

使う場合はコード量だけでなく型推論が効くか、テストをどう書くか等を調べる必要がありそうです。

まとめ

createAsyncThunkEntityAdapterは良さそうでした。

  • 公式提供の安心感
  • 型による保守性
  • 実装で迷わないオピニオン

去年ThunkActionの型解決で苦労したので、今年はRedux Toolkitを推していきたいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React+Reduxアプリを仕事で開発して苦労したこと

React+Reduxアプリを仕事で開発したので、苦労したことをまとめます。
これからReactのプロジェクトを始める方への参考となれば嬉しいと思っています。

誤った解釈などありましたら、コメントでご指摘お願いいたします。

対象読者

  • Reactの基礎をある程度勉強済みの方
  • これからReactで新規プロジェクトを立ち上げる開発者の方

使用した主な技術

TypeScript / React / Redux / redux-observable / RxJS

記事投稿者のスキルと開発内容

React歴は3か月程度です。
その前はAngularJSで書かれたコードベースの簡単な修正をやっており、Webアプリ開発の業務歴は1年程度です。

今回の開発はAngularJSで書かれたコードベースのReactへのリプレイスと追加機能の開発になります。
開発体制としては、開発ベンダーの開発者約4名が主体+発注元企業に勤務している私(一応SE兼プラグラマ―)の共同開発になります。(少人数にも関わらず2つの企業で開発というのは珍しいのでしょうか。)

苦労したこと

1. Reactの実装方針やコーディング規約

ClassコンポーネントとFCコンポーネントの混在。私が開発する部分はFCで統一。

Reactは初めてで技術をキャッチアップしながら、なるべくモダンでディファクトスタンダードな方法を探しました。

昨今のReactはHooksによるFCコンポーネントが主流とあり、「既にあるClassコンポーネントはそのままでもよいので、新規で実装する機能についてはFCコンポーネントでどうか」と開発ベンダーに提案しましたが、納期や学習コストの観点で難しいといわれてしまい、私の担当箇所のみFCコンポーネントで実装することになりました。

私がFCで実装した理由としては、今後Hooks機能は充実していくと思いますし、Classなど初学者が躓く箇所を無くし、なるべき敷居低くしたいと考えたためです。(コードベースとしては混在してしまう為、逆に混乱を招くかもしれませんが・・・今後のリファクタリングで修正しようと思っています)

ESLintのアラートは修正する、コードフォーマッター(Prettier等)は使用しない。(React関係ないです)

この点については全く開発ベンダーと話し合うことができず(私のキャッチアップ不足や納期の関係上)、コードのインデントをコードベース全体で統一することができませんでした。
開発ベンダー側がインデントはあまり気にしていないのか、規約としてESLintのみを取り入れており、そこに従いました。

自身が新規開発したファイルはprettierで整形していますが、やはりインデントは全体で整えたほうが見やすいので、ルールを最初にがっちり決める、もしくは最後に修正するなど、計画しておくべきでした。

2. ライブラリの選定やバージョンのアップデート

非同期処理のミドルウェアについて

非同期処理のミドルウェアは、redux-thunkredux-sagaではなく、redux-observableを使用することになりました。開発ベンダー側がボイラーテンプレート作成時にライブラリ選択したことですが、私としてはRxJSの学習コストの高さからredux-thunkの使用を提案しましたが、こちらも納期の関係上、既にredux-observable、RxJSで実装済みの箇所をredux-thunkに置き換えるのは難しかったです。そのため、私もRxJSで書くことにしました。

RxJSを使用した感想としては、非同期の処理がとても簡潔に書ける良いライブラリでした。
今のところRxJSを起因としたバグもなく、やはり最初の学習コストというのが大きな壁かなと思います。

またRxは他の言語でも多数あり(RxJavaやRxSwiftなど)、実務で使用できたのは良い経験になりました。
今回の開発で機能要件にあったポーリング処理なども簡単に実装することができました。

これから導入を検討される場合は、アプリの規模や複雑な非同期処理があるか等を考えて慎重に決める必要があると思います。

古いライブラリはできる限りアップデートする。

開発ベンダー側の過去プロジェクトで作成したと思われるボイラーテンプレートを使用して開発が始まったため、ライブラリのバージョンが古かったり、未使用のライブラリがいくつかありました。(recompose(今後メンテナンスされないと発表されている)、redux-starter-kit(redux-toolkitに名前が変わり正式版リリース済み)など)

指摘しないとおそらく修正されずに納品されていたため、早い段階で指摘し修正していただきました。
※今後、弊社でも開発をしていくという前提もありましたし、古いバージョンを使い続けるメリットはないため、できる限り気づいた点を指摘しました。

3. Reduxストアの扱いについて

コンポーネント内で完結するステートはuseStateを使用する。ドメイン単位(DBの複製となる)データはReduxストアに保存し、redux-persistを用いてLocal Storageに保存する。

useStateを使う理由としては、すべてのデータをReduxストアで一元管理にした場合、単一の値を更新する処理を追加するだけでも、「reducer、action、epic(redux-observable)、slice、selector(redux-toolkit)」などを書く必要があり大変だったのと、ドメイン単位とコンポーネント単位のステートを分けれるという、メリットがあったためです。

テスト時には、「DBの値を更新してもReduxストアの値が更新されておらず、画面の表示結果が古い」みたいなバグもあったため、開発の最初にちゃんと決めておくべきことだったと思います。

最後に

Reactの開発や共同開発で大変だったことは他にも沢山ありますが、とりあえず上記3点を書かせていただきました。(今後ほかにも追加するかもしれませんが)

最近はReactやTypeScriptを通してはじめてプログラミングを楽しいと感じているところです。
styled-componentsやStorybookなどが気になっているので、調べて導入の価値があれば挑戦しようと思っています。

その他、React開発の取り組みでうまくいったことなどがあればご教示いただけると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-native-expo でローカル開発

ローカルホストサーバに繋げない

Expo で fetch を使用して、ローカルで起動中のAPIにGETできないなぁ
と思っていたら
グローバルに公開されているエンドポイントならリクエストがうまく行った

なんか、どこかの設定をTrueにしないと行けないらしいのだけれど、よく分からん
eject しなくちゃいけないのなら EXPO 使う意味ないし・・・

だから localhost じゃなくて、大人しくプライベートIP使えば良いか
という事で、設定のメモ

設定

MacBook-Pro ~ % networksetup -getinfo Wi-Fi
DHCP Configuration
IP address: 192.168.11.2

fetch でのリクエストの向き先IPをコレにして

バックエンドフレームワーク側でもホワイトリストにこのIPを登録して
あとは起動するだけ

Django

./manage.py runserver 192.168.11.2:8000

色々触ってると、こんなちっちゃいことも忘れるからメモ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SharePoint Onlineだけを使って React で作ったWebアプリケーションを公開する

背景・目的

SharePointの拡張に関しては、SPFxを使おうってことなんですが、なかなかに難しそうで学習意欲がわかない!
ということで、ずぼらにReact製のWebアプリケーションをSharePoint Onlineで無理やり動作させてみました。

完成イメージ

zubora.gif

注意

SharePointのドキュメントライブラリでは、配置された.html、.aspxのファイルはダウンロードされてしまうのですが、.aspxのファイルはカスタムスクリプトの実行を許可することで、ダウンロードされず画面で表示させることができるという、2020年2月時点で有効な仕様を前提に動作させています。

参考にさせていただいたブログ(外部リンク)
idea.toString();
静的 HTML ファイルを SharePoint にアップロードして公開する
Art-Break Tech
チームサイトやコミュニケーションサイトではライブラリにアップロードした.aspxファイルはクリックすると表示されずにダウンロードされてしまう

用意したもの

  • SharePoint Online
  • Node.js (lts)入りのパソコン(Windows10)とインターネット回線

やってみたこと

  1. SharePoint Onlineで任意のサイトを作成
  2. カスタムスクリプトの実行を許可
  3. ドキュメントライブラリを作成
  4. npmをインストールしReactの実行環境を作成(yarn create react-app使います)
  5. ちょっとした開発
  6. build
  7. buildしたファイルの加工
  8. ドキュメントライブラリに配置

やってみたことの説明

1.SharePoint Onlineで任意のサイトを作成

GUIでポチポチ。ちなみにコミュニケーションサイトで空白を選択しました。
image.png

2. カスタムスクリプトの実行を許可

記載させていただいたブログを参考に以下のコマンドをSharePoint Online Management Shellで実行します。

Connect-SPOService -Url https://**********.sharepoint.com -Credential **********
Set-SPOsite https://**********/sites/zubora -DenyAddAndCustomizePages 0

3. ドキュメントライブラリを作成

GUIでポチポチ。

4. npmをインストールしReactの実行環境を作成(yarn create react-app使います)

以下のコマンドをコマンドプロンプトで実行します。

yarn create react-app zubora

以下が表示されたらダウンロード成功です。

Success! Created zubora at **********
Inside that directory, you can run several commands:

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd zubora
  yarn start

Happy hacking!
Done in 63.69s.

次に以下のコマンドを実行します。

cd zubora
yarn start

この画面が出たら成功です。
image.png

今回はせっかくなので、material-uiのコンポーネントを使ってSharePointっぽくない感じにしてみたいと思います。
そのため、以下のコマンドを追加で実行します。

yarn add -D @material-ui/core @material-ui/icons

5. ちょっとした開発

デフォルトで作成されるファイルを修正します。

index.htmlの26行目の下に以下を追加します。

index.html
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />

package.jsonの4行目の下に以下を追加します。

package.json
homepage": "./", 

App.jsを以下に書き換えます。{your sharepoint site}は自身のサイトUrlに変更してください。

App.js
import React from 'react';
import Button from '@material-ui/core/Button';
import Container from '@material-ui/core/Container';
import CircularProgress from '@material-ui/core/CircularProgress';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Badge from '@material-ui/core/Badge';
import MailIcon from '@material-ui/icons/Mail';
import Avatar from '@material-ui/core/Avatar';
import AssignmentIcon from '@material-ui/icons/Assignment';

import LoginName from './LoginName';

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      loginName: 'Anonymous'
    }
  }
  getSpLoginUser = () => {
    fetch('{your sharepoint site}/sites/zubora/_api/web/currentuser', {
      'method': 'GET',
      'headers' : {
        'Content-type': 'application/json; odata=verbose',
        'Accept': 'application/json; odata=verbose'
      }
    })
    .then((response) => response.json())
    .then((responseJson) => {
      this.setState({ loginName: responseJson.d.Title })
    })
  }
  render() {
    return (
      <div>
        <AppBar position="static">
          <Toolbar>
            <Typography variant="h6">
              <LoginName dispLoginName={this.state.loginName} />
            </Typography>
          </Toolbar>
        </AppBar>
        <Container maxWidth="sm">
          <p>Create React Appで作ったよ</p>
          <p>すでにSharePointにログイン(サインイン)しているので特別な認証設定なしにRestも使えます</p>
          <Button variant="contained" color="primary" onClick={this.getSpLoginUser}>
            SharePoint Onlineのログインユーザを画面上部に表示
          </Button>
        </Container>
        <Container maxWidth="sm">
          <p>material ui のコンポーネントも使えるよ</p>
          <div>
            <p>CircularProgress</p>
            <CircularProgress />
          </div>
          <div>
            <p>Badge</p>
            <Badge badgeContent={4} color="secondary">
              <MailIcon />
            </Badge>
          </div>
          <div>
            <p>Avatar</p>
            <Avatar>
              <AssignmentIcon/>
            </Avatar>
          </div>
          <div>
            <p>などなどたくさん!!!</p>
          </div>
        </Container>
      </div>
    );
  }
}
export default App;

App.jsと同じフォルダにLoginName.jsを作成します。

LoginName.js
import React from 'react';

class LoginName extends React.Component {
  render() {
    const dispLoginName = this.props.dispLoginName;
    return (
      <div>現在ログインしているユーザは {dispLoginName} さんです。</div>
    );
  }
}
export default LoginName;

6. build

以下のコマンドを実行します。

yarn build

以下みたいになれば成功です。

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  66.63 KB (-2.14 KB)  build\static\js\2.7ec70346.chunk.js
  1.26 KB (-87 B)      build\static\js\main.5929dd22.chunk.js
  771 B                build\static\js\runtime-main.7cf92c16.js
  278 B                build\static\css\main.5ecd60fb.chunk.css

The project was built assuming it is hosted at ./.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.

Find out more about deployment here:

  bit.ly/CRA-deploy

Done in 9.33s.

7. buildしたファイルの加工

buildフォルダの中に、index.htmlがあると思いますので、
index.html → index.aspx
にリネームします。

8. ドキュメントライブラリに配置

buildフォルダ中身の以下を除いて配置します。
- asset-manifest.json
- manifest.json

これで、index.aspxにブラウザでアクセスすると動作します。

と思ったらブラウザかにブラウザの設定によって(私はEdgeで確認。Chromeは問題なし)はエラー(CORS)が出るみたいですが、その場合以下のコードを削除してください。

index.aspx
<link rel="manifest" href="./manifest.json" />

まとめ

無事に上記手順のみで、 SharePoint Online だけで動作させることができました。
もし試される場合は、開発する際の認証の設定とか、Reactでルーティングした場合のURLとファイルの関係とか、いくつか留意事項がありますのでご注意ください。
また、あくまで現時点での SharePoint Online の仕様で許されているだけで、今後.aspxのファイルの扱いこのままかどうかは何とも言えません。
もし動作できなくなった場合には、素直にWebサーバに資産の移行か、SharePointの標準機能のページへの移行を検討しましょう。
ですがお手軽に組織内にWebアプリケーションを公開してみたい、複雑な認証の設定はしたくないが、 SharePoint REST サービスを組み合わせたWebアプリケーションを公開してみたいとかの場合にはやってみてもよいのではないでしょうか。

感想

SPFxに助走をつけて飛び込むためのReact勉強だったのですが、Reactのほうがおもしろくなりつつあります笑
以上、初Qiitaでした。レベルが低いかもですが、第一歩ということでお手柔らかに。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firestore エミュレータのデータをローカル環境で import/export する

概要

Firebase エミュレータで Firestore の import/export ができるようになったので、導入方法について紹介します。これまで、エミュレータを再起動する度にデータが初期化されていていましたが、特定のタイミングで export して起動時に import できるので、毎回テストデータを取り込む作業がなくなるなど、開発効率が上がります。

導入方法

2020/02/24 現在、この機能は firebase/firebase-tools の master ブランチにマージ済みですが、 release のバージョンは出ていないようなので、 master ブランチを指定して npm install します(該当プルリク: Import/export support for emulators #1968)。 firebase-tools のリリースは頻繁に行われているようなので、すぐに master ブランチ指定をしなくてもインストールできるようになると思います。

npm install -g firebase/firebase-tools#master

以下のコマンドを叩いて Export コマンドのヘルプが正常に表示されれば OK です。

firebase emulators:export --help
Usage: firebase emulators:export [options] <path>

export data from running emulators

Options:
  --only <emulators>  only specific emulators. This is a comma separated list of emulator names. Valid options are: ["functions","firestore","database","hosting","pubsub"]
  --force             Overwrite any export data in the target directory.
  -h, --help          output usage information

実行方法

Firestore エミュレータのデータを export

firebase emulators:export コマンドで data ディレクトリ配下に export されます

firebase emulators:export ./data/

すでにデータがある場合には上書きするか確認されますが、毎回上書きで問題なければ --force オプションを指定します。

firebase emulators:export --force ./data/

Export されたデータを読み込んで Firestore エミュレータ起動

firebase emulators:start コマンドで --import オプションをつけて export 先の data ディレクトリを指定することで、データを読み込んで起動できます。

firebase emulators:start --import=./data --only firestore

必要に応じて、上記コマンドを package.json の scripts に追加したり(Web 開発の場合)、 data ディレクトリを .gitignore に追加しておきましょう。

まとめ

以上、 Firestore エミュレータデータの import/export 方法を紹介しました。

特定の検証ケースごと(インストール直後、チュートリアル直後、インストール翌日の起動時など)に export 先を分けると、よりデバッグが捗りそうですね。

関連記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む