20191219のReactに関する記事は17件です。

GitHub Actionsを使ってReactアプリのCI環境を構築(Lighthouse CIも)

この記事はフラー Advent Calendar 2019 の 19 日目の記事です。
前日の18日目はShota Kobayashiで、「快適なUI開発生活を提供する Angular CDK の Portal と Overlay」でした。

はじめに

私はフラーでフロントエンドエンジニアをしています。
最近はあまり新しい技術のキャッチアップができていなかったので、今回のAdvent Calendarでは気になっていた技術をいろいろと試してみました。

この記事では、すでにFirebase Hostingで公開しているReactのWebサイトを、GitHub Actionsを使ってCIできるようにする方法について紹介します。
また、あまりにも簡単にできてしまったのでLighthouse CIの設定もしてみました。

React Webサイト

ReactのWebサイトはCreate React Appで作成しており、npm run buildするとbuildディレクトリにアプリが生成されます。
https://github.com/nobux42/portfolio-react

また、Firebase Hostingの設定もすでにされいます。
Firebase Hostingの設定についてこちらを参照ください:
https://qiita.com/junara/items/74801923ca108b328b26

GitHub Actionsの設定

1. GitHub Actions のページを開く

GitHubのリポジトリで[Actions]ページを開きます。
スクリーンショット 2019-12-19 22.53.20.png

2. テンプレートの作成

[Node.js]のテンプレートを選択します。
スクリーンショット 2019-12-19 23.08.05.png
.github/workflow/nodejs.yaml というファイルが作成されます。

3. Firebaseにデプロイするための設定を追加

  • steps に以下を追加
     - name: deploy to Firebase Hosting
        run: |
          npm install -g firebase-tools
          firebase deploy --token=${{ secrets.FIREBASE_TOKEN }}

4.Firebase トークンの設定

まずはローカルでコマンドを叩いてトークンを取得します。

$ firebase login:ci
****

GitHubリポジトリページに移動し、トークンを登録。

  • [Settings] > [Secrets] > [Add a new secret]
    • Name: FIREBASE_TOKEN
    • Value: ****

ちなみに firbase.json は以下のようになっています。

["firbase.json"]

{
  "hosting": {
    "public": "build",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

試してみる

ローカルからプッシュしてみます。

$ git push origin masger

スクリーンショット 2019-12-16 7.26.05.png

うごきました。
無事にデプロイもされました。

たったこれだけで、無事にビルドされてFirebaseにもホスティングされてしまいました。
個人でCIを構築するのにもGithub Actionsはとてもおすすめだと思います。

Lighthouse CI

ちょっと簡単すぎたので、LighthouseをGithubアクションに組み込んでパフォーマンスチェックもしたいと思います。

LighthouseとはGoogleのSEOチェックツールで、CI用にLighthouse CIというのも用意されていると聞いたので早速組み込んでみました。

以下の設定をワークフローに設定することで簡単に動作させる方法もありますが、

   - name: run Lighthouse CI
        run: |
          npm install -g @lhci/cli@0.3.x
          lhci autorun --upload.target=temporary-public-storage || echo "LHCI failed!"

CI上でエラーなどを出したかったため、.lighthouse.jsonという設定ファイルを用意してパフォーマンスに問題がある場合はエラーになるようにしました。

[".lighthouse.json"]

{
  "ci": {
    "collect": {
      "numberOfRuns": 3,
      "startServerCommand": "npm run start",
      "url": [
        "http://localhost:3000/",
        "http://localhost:3000/about"
      ]
    },
    "assert": {
      "assertions": {
        "offscreen-images": "off",
        "uses-webp-images": "off",
        "first-contentful-paint": ["error", {"maxNumericValue": 10000}]
      }
    },
    "upload": {
      "target": "temporary-public-storage"
    }
  }
}

ワークフローへ追加した設定は以下になります。

  • steps に以下を追加
    - name: run Lighthouse CI
      run: |
        npm install -g @lhci/cli@0.3.x
        lhci autorun --config=./.lighthouserc.json

最終的なワークフローは以下のようになりました

["nodejs.yaml"]

name: Node CI

on:
  push:
    branches:
      - master
jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [10.x]

    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - name: npm install, build, and test
      run: |
        npm ci
        npm run build --if-present
      env:
        CI: true
    - name: run Lighthouse CI
      run: |
        npm install -g @lhci/cli@0.3.x
        lhci autorun --config=./.lighthouserc.json
    - name: deploy to Firebase Hosting
      run: |
        npm install -g firebase-tools
        firebase deploy --token=${{ secrets.FIREBASE_TOKEN }}

Lighthouse CIでは解析結果を自前の公開サーバーに格納して、簡単に確認できるようになっています。
スクリーンショット 2019-12-19 23.39.13.png
(色々と問題があるのがわかったので、少しずつ修正していきたいと思います。)

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

DECK.GLを使ってGoogleMapタイムラインをビジュアライズしてみよう!#4 〜Mapコンポーネントの表示〜

DECK.GLを使ってGoogleMapのタイムラインをビジュアル化してみよう!の第4弾の記事です。

前回はDECK.GLのPathLayerを使って、データの描画を行いました。
今回はこれにマップコンポーネントを組み込んで表示させてみたいと思います。

前回のおさらい

前回は、App.jsx に下記のコードを書きました。

App.js
import React from "react";
import DeckGL from "@deck.gl/react";
import { PathLayer } from "@deck.gl/layers";

// Viewport 初期設定
const viewState = {
  longitude: -122.5,
  latitude: 37.75,
  zoom: 11
};

// 描画する地理データ
const data = [
  {
    path: [
      [-122.45, 37.7],
      [-122.5, 37.8],
      [-122.6, 37.7]
    ]
  }
];

export default class App extends React.Component {
  render() {
    const layer = [
      new PathLayer({
        id: "path-layer",
        data,
        getPath: d => d.path,
        getWidth: 30
      })
    ];
    return <DeckGL viewState={viewState} layers={layer}></DeckGL>;
  }
}

マップコンポーネント

DECK.GLはマップコンポーネントなしでも独立して動作しますが、地理データをビジュアライズするのに地図情報が表示されないのではおもしろくありませんよね。

DECK.GLと簡単に連携できるマップコンポーネントとして、GoogleMapとMapboxの2つの選択肢がありますが、
今回はMapboxを使って描画したいと思います。

※マップコンポーネントの連携についての詳細はこちらをご参照ください。

Mapboxとは?

Mapboxとは、米カリフォルニア州サンフランシスコにある位置データプラットフォームです。要するに、GoogleMapのような地図サービスです。

最近はゼンリンと提携したり、「ソフトバンク・ビジョン・ファンド」から出資を受けたりと、日本での事業も強化しようとしているようです。

Mapboxでは、GoogleMap同様開発者に便利なインターフェースがたくさん提供されています。
例えば、Mapbox GL JSというJavaScriptライブラリが提供されていたり、
mapbox-glという名前でNodeパッケージも公開されています。

今回はReactコンポーネントとして利用できるreact-map-glを使ってMapboxを連携させましょう。

※MapboxのDocumentationはこちら
- Mapbox GL JSのGithub
- react-map-glのGithub

Mapboxを連携させよう

react-map-glライブラリのインストール

まずreact-map-glライブラリをインストールします。

$ yarn add react-map-gl
yarn add v1.17.3
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
warning " > @testing-library/user-event@7.1.2" has unmet peer dependency "@testing-library/dom@>=5".
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
[4/4] ?  Building fresh packages...
success Saved lockfile.

... (長いので省略)

├─ tinyqueue@2.0.3
├─ vt-pbf@3.1.1
└─ wgs84@0.0.0
✨  Done in 24.60s.
$ 

MapGLコンポーネントのimport

インストールが終わったら、MapGLコンポーネントをimportしましょう。
また、Mapbox GL JS の css も import しておかないとwarningが出てしまうので、こちらもimportします。

App.js(一部)
import React from "react";
import DeckGL from "@deck.gl/react";
import { PathLayer } from "@deck.gl/layers";
import MapGL from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";

Mapbox のアクセストークン

Mapbox を使う場合は、 Mapbox のアクセストークンを埋め込む必要があります。

アクセストークンは、Mapboxにログインして、「Access tokens」の + Create a token ボタンから作成することができます。

Screen Shot 2019-12-19 at 22.32.55.png

こちらからログインしてください :point_right_tone1: https://account.mapbox.com/
※アカウントを持っていない人は、アカウントの作成が必要です

アクセストークンが取得できたら、下記のようにApp.jsxに書きます。
本来アクセストークンのような外部に漏れてはいけない情報は、.envファイルなどの環境変数で管理した方が良いですが、
Mapboxには、指定したURLから発信されたリクエストに対してのみ機能する「制限付きトークン」を作成することができるので、これであればファイルに埋め込んでも問題ありませんね。

App.js(一部)
// mapbox access token
const MAPBOX_ACCESS_TOKEN = "pk.eyJ1IjoiZ......."

MapGLコンポーネントの追加

それでは最後にDeckGLにマップコンポーネントを組み込みます。
上記で定義したアクセストークンはmapboxApiAccessTokenに渡します。

App.js(一部)
    return (
      <DeckGL viewState={viewState} layers={layer}>
        <MapGL mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN} />
      </DeckGL>
    );

ブラウザで表示

それではここまで書いたコードをブラウザで表示してみましょう!
yarn startコマンドで起動させると・・・

:point_down_tone2::point_down_tone2::point_down_tone2:

Screen Shot 2019-12-19 at 22.22.24.png

地図の上にパスが表示されました!!:clap:

次回予告

次回からはいよいよGoogleMapタイムラインの表示を行なっていきます!

App.jsxの全体像

App.js
import React from "react";
import DeckGL from "@deck.gl/react";
import { PathLayer } from "@deck.gl/layers";
import MapGL from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";

// mapbox access token
const MAPBOX_ACCESS_TOKEN = "pk.eyJ1IjoiZ.......";

// Viewport 初期設定
const viewState = {
  longitude: -122.5,
  latitude: 37.75,
  zoom: 11
};

// 描画する地理データ
const data = [
  {
    path: [
      [-122.45, 37.7],
      [-122.5, 37.8],
      [-122.6, 37.7]
    ]
  }
];

export default class App extends React.Component {
  render() {
    const layer = [
      new PathLayer({
        id: "path-layer",
        data,
        getPath: d => d.path,
        getWidth: 30
      })
    ];
    return (
      <DeckGL viewState={viewState} layers={layer}>
        <MapGL mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN} />
      </DeckGL>
    );
  }
}

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

github-pageにReact appをデプロイしてみた

はじめに

Reactを勉強する上でGithub pageでReactアプリを公開できるのか気になったのがきっかけです。

GitHub Pagesについて
GitHub Pages は、GitHub のリポジトリから HTML、CSS、および JavaScript ファイル を直接取得し、任意でビルドプロセスを通じてファイルを実行し、ウェブサイトを公開できる静的なサイトホスティングサービスです。
[GitHub 公式ヘルプ]https://help.github.com/ja/github/working-with-github-pages/about-github-pages

参考にしたサイト

環境

以下は私が行った環境です.

name version
Node v10.14.2
npm 6.4.1

2019/12/19現在ではReact公式サイトに

あなたのマシンに Node >= 8.10 及び npm >= 5.6 の環境が必要です    

と記載されていました.

手順

1.Reactアプリの作成

以下のコマンドを実行しました.

今回はreact-pagesという名前のReactアプリを作成しています.

$ npx create-react-app react-pages

2.gh-pagesのインストール

以下のコマンドでgh-pagesをインストールします。
以下のコマンドではcd react-pagesで作成したreact-pagesフォルダまで移動しgh-pagesをインストールしています。

$ cd react-pages
$ npm install gh-pages --save-dev
npm

Node.jsで利用するパッケージ管理ツール

gh-pages

GitHubのgh-pagesブランチ(または他の任意の場所)にファイルを公開するためのパッケージ

3.Reactアプリのpackage.jsonを修正

作成したReactアプリのpackage.jsonに以下の設定を追記してください.
ここで"homepage"に書いてあるURLの[GitHub username],[created repository name]はそれぞれ自分が作成した名前をつけてください

package.json

//...
"homepage": "https://[GitHub username].github.io/[created repository name]/",
"scripts": {

    "predeploy": "npm run build",
    "deploy": "gh-pages -d build"
  },
//...

4.GitHubでRepositoryを作成

このときのリポジトリの名前は作成したReactアプリと異なっていても大丈夫です

また、GitHubがPROアカウントではない場合はPublicでRepositoryを作成してください。

5.GitHubPageの設定

作成したGigHubのリポジトリよりsettingへ移動,その後下へスクロールしGitHub Pages のSourceをmaster branchへ変更する.

Screenshot from 2019-12-19 21-50-22.png

Screenshot from 2019-12-19 22-05-34.png

6.作成したアプリをデプロイ

以下のコードでデプロイします。また,実行する場所は作成したReactアプリ内で行ってください.以下の前半部分はリポジトリを作成したときに出てくる任意のフォルダーとリポジトリを接続しREADME.mdをリポジトリへ追加しているものです.

$ git init
$ git add README.md
$ git commit -m "first commit"
$ git remote add origin 
$ git@github.com:[GitHub username]/[Repository name].git
$ git push -u origin master
$ npm run deploy

結果

GitHubPages設定後表示もう一度GitHubPagesの設定場所へ移動し表示されているURLをクリック.

以下のようなページが開かれていれば成功です.

Screenshot from 2019-12-19 22-11-25.png

最後に

デプロイの意味とか全然しらない初学者(私)でもちょっと調べたらできるもんだなぁと感激しました。
積極的にいろんなことにトライしてみようと思いました。
次はReactをつかって自分のポートフォリを作りたいと思います。

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

「スタートアップ×React LT大会 Coral Developers Night」に参加してきた

はじめに

スタートアップ×React LT大会 Coral Developers Nightに参加してきたのでそのことについてまとめます。タイトルの通り今回はReactに関するLTを聴いてきました。

株式会社justInCase 「LT1」

アトミックデザインについての技術紹介でした。以下のツールを主に使っているそうです。

デザイナーとの連携もant.designの機能ベースで進めるとのこと。

水上 駿 / DIGGLE株式会社 「Immutable JSを使ったスケーラブルなReactアプリケーション開発」

オブジェクトの値が不変で扱うことのできるImmutable JSについての紹介をされました。Immutable JSはListという形を扱い、Setという重複のない集合を扱うことなどのことができるそうです。
オブジェクトが不変であることで誰かが破壊的な変更をするという事故もなくなるので、開発の余計な工数が減るというメリットがあるとのこと。

髙橋 耕太郎 / 株式会社Handii 「Reactの非同期処理のこれからを考える」

redux-sagaの話でした。このsagaを使うことでComponentとタスクを分離して書くことができる。このsagaは公式のライブラリが充実している。しかしsagaは影響と副作用を一気に引き受けるので扱いが難しいという問題もあるとのこと。
また、今後はhooksでの非同期処理も検討しているとのことでした。

森脇 和也 / 株式会社空 「スタートアップでReactを使うってどうなの? 」

タイトルの通りReactをスタートアップの事業で用いたという話です。ここのLTで重要となったワードはPMFという言葉で、なにの課題を解決したいのかを第一に考えるのが大事とのこと。

株式会社justInCase 「LT2」

フロントエンドのデプロイの話と、Reactアプリの多言語化対応の話でした。多言語対応にはvalidator.jsreact-intlを用いて行ったとのこと。

ゲストLT1「yarn upgaradeも怖くないテストをつくる」

ライブラリを常に最新の状態でも怖くないように、テストを継続的に行うという話でした。そのためにロジックとビューを分離させるようなコードの書き方をするなどの工夫を行っているとのこと。

ゲストLT2「俺のお気に入りhooksランキング」

  • useArray
  • useDebouncedQuery
  • useFetch系
  • useInterSectionObserver
  • useToast, Drawer, ModalができるuseContext

さいごに

資料はおそらく後日イベントページに反映されるのでは?と思っています。

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

なぜReactでもVueでもなくElmを使っているのか

はじめに

Leverages(レバレジーズ)Advent Calendar 2019 22日目担当の坂田です。

お仕事では自社メディアである、フリーター、第二新卒向け就職エージェントサイトのシステムや人材情報の管理システムの新規開発、保守運用などをしています。

今回は自社メディアのエントリーフォームを開発するにあたりフロントエンドフレームワークであり、言語でもあるElmという技術を利用した経緯について書きます。

かなりの人がElmに関して知っていることが少なかったりElmという言葉を聞いたことがないかと思われますが、参考程度に聞いてやってみたいと思う人が増えればと思います。

この記事ではElmの技術的なお話や、具体的なお話は一切しません。

結論

Elmはアンチパターンを実装できない。人の目を介さず、人的リソースを使わずにソフトウェア品質の最低ラインを強力な形で担保できるため。

なぜReactでもVueでもなくElmを使っているのか

仮定

仮定1. 各コンポーネント等に状態を持たせることは問題である

大規模なアプリケーションは、多くの状態が色々なコンポーネントに散らばったり、コンポーネント間の相互作用のために複雑になりがちです。この問題を解消するために ...

引用元 https://jp.vuejs.org/v2/guide/state-management.html

仮定2. 関数型、型付けは強く、静的であるほどバグが減る

言語のカテゴリ毎の不具合の作りやすさの調査

指向 動的静的 型付けの強さ メモリ管理の有無 低いほど高得点
関数型 静的 強い型付け -0.25
関数型 動的 強い型付け -0.17
手続き型 静的 強い型付け −0.06
スクリプト 動的 強い型付け 0.001
スクリプト 動的 弱い型付け 0.04
手続き型 静的 弱い型付け 0.14

Functional-Static-Strong-Managed −0.25 (0.04) ∗∗∗
Functional-Dynamic-Strong-Managed −0.17 (0.04) ∗∗∗
Proc-Static-Strong-Managed − 0.06 (0.03) ∗
Script-Dynamic-Strong-Managed 0.001 (0.03)
Script-Dynamic-Weak-Managed 0.04 (0.02) ∗
Proc-Static-Weak-Unmanaged 0.14 (0.02) ∗∗∗

各カテゴリに属する言語は以下のとおりです。

Functional-Static-Strong-Managed: Haskell、Scala
Functional-Dynamic-Strong-Managed: Clojure、Erlang
Proc-Static-Strong-Managed: C#、Java、Go
Script-Dynamic-Strong-Managed: Python、Ruby
Script-Dynamic-Weak-Managed: Perl、PHP、JavaScript
Proc-Static-Weak-Unmanaged:C、C++、Objective-C

引用元 https://developers.srad.jp/story/14/11/08/081210/

但しこの記事の仮定では各言語の具体的なポイント数は考慮しないものとして、カテゴリ毎のポイント数のみ見ることにします。

仮定3. Nullを他の型の一部として扱うのは問題である

2009年のカンファレンスでNull参照を発明したことについて謝罪している。

それは10億ドルにも相当する私の誤りだ。null参照を発明したのは1965年のことだった。当時、私はオブジェクト指向言語 (ALGOL W) における参照のための包括的型システムを設計していた。目標は、コンパイラでの自動チェックで全ての参照が完全に安全であることを保証することだった。しかし、私は単にそれが容易だというだけで、無効な参照を含める誘惑に抵抗できなかった。これは、後に数え切れない過ち、脆弱性、システムクラッシュを引き起こし、過去40年間で10億ドル相当の苦痛と損害を引き起こしたとみられる。

引用元 アントニー・ホーア - wikipedia

これら上記の3つの仮定に関して、一般的にエンジニアの皆さんには共通した意識があると思います。

仮定1について

仮定1. 各コンポーネント等に状態を持たせることは問題である

ElmというフレームワークはVueで言うところのvuexReactでいうところのReduxなどに用いられているアーキテクチャを標準搭載 & それ以外のアーキテクチャを利用できない仕組みになっており、基本的に状態を一元管理することしかできません。
なおvuexとReduxはElmアーキテクチャに触発されたものであり、公式に記述しています
https://jp.vuejs.org/v2/guide/state-management.html
https://github.com/reduxjs/redux
リンクに飛んでブラウザでElmと検索してみましょう

そして現在の時刻を取得する関数やhttp通信などの関数は毎度同じ関数とは言えず、状態保有した関数と言えます。
Elmはそれらを関数として使うことができません。(便利に使えて関数とは違う仕組みとして利用することはできます)

ReactやVueでElmアーキテクチャを利用していたとしても、JSやTypeScriptの言語仕様として、状態の保持を頑張ればできてしまうんですね。

最大の違いは上記の、状態を保持することができないこと、純粋関数以外を記述できないという点を、言語仕様として厳しくコンパイラがチェックしている点にあります。

その為、コードレビューやコーディング規約などの人的リソースを使う必要なく、品質の高いソフトウェアを記述できるという利点があります。

仮定2について

仮定2. 関数型、型付けは強く、静的であるほどバグが減る

VueとReactは最大でこれらの条件に近づくには、Any型を含み、手続き型であるTypeScriptを利用することがJavaScriptからの精一杯の解決法です。

Elmは関数型であり、強い型付けであり、静的です。

仮定3について

仮定3.Nullを他の型の一部として扱うのは問題である

仮定2に近く見えるかもしれませんが、強い型付けだとしても、Javaのような言語ではNullが存在します。これらは仮定2の論文では分類されていません。その為違う話題として取り上げています。

ElmではJavaScriptとは異なり、Nullが存在しません。Intという型と、IntとNullの直和の型を完全に別のものとして捉えており、問題を解決しています。

なお、TypeScriptでもこの問題は解決しています。
しかしanyがあるのでやはりコードレビューリソースが追加でかかることや書き手によりコードの質が大きく変わってしまうのではという問題が付き纏います。

つまり

全体的にElmという言語は開発者やコードレビューの質に依存せず品質を向上させ、ソフトウェアの品質の最大値を上昇させるというものではなく品質の最低ラインを担保するという性質がありそうです。

仮定を真とし、これらの課題を解決することに絞るとするならば、Elmに帰結することは常と言えます。
(ここではReactやVueをディスっているように見えますが、任意の仮定と課題に絞り勝っている部分があるというだけです。)

現実的には

しかし、実際にElmを実戦投入するとなるとReactにはあるパッケージがElmにはなかったり、そもそも記法がML系言語だったりと大多数のエンジニアには受け入れがたい現実があります。

パッケージに関してはそこそこ数も揃っている上に、JSと純粋さを保ちつつ連携できる機能がElmには備わっています。

そして慣れてしまえばML系の記法は個人的にはAlgol系よりも書きやすいものですし、DXも非常に高いと思ってはいますが、チームで開発する以上は他の人の慣れも考慮する必要がありますね。

その為、今回新たに開発したElmを利用したプロジェクトに関しては、全てのページをElmで置き換えるようなことはなく、適材適所で特にロジックや非同期が複雑になりがちなページに絞ってElmを利用することにより、チームとして最大限の効果を享受しようと試みています。
(なお、Elmは非常に言語機能がコンパクトかつシンプルに纏まっており、非常に学習コストの低い言語と言えます。)

Elmは小さく始めることも可能なのです。

個人的には

存在しないパッケージを自分で開発したりとか、フレームワークや言語にバグが存在すれば自分で治せるし、そんなことよりもそもそも直しようがない言語の構文や機能自体から波及するメリットの優位性を重視して他の言語やフレームワークではなくElmを利用しています。

さらに、最近のオブジェクト指向への懐疑的な流れから流行っているモダンな言語機能をもつRustやScalaなどといった言語と同じような代数的データ型やnullの扱いについても学べ、開発者自身の市場価値や技術力を向上させることも利用した動機の一つになっています。

個別具体的な用法についてのコード他の言語とは異なりネットに転がってないので、コピペして機能開発をすることはできませんが、正しく理解していれば、開発可能性(計算可能性に掛けた今作った言葉)はReactやVueと全く同じなので、ぜひ挑戦してみてください。

まとめ

Elm、はっきり言ってコードレビューの工数減るし、実行時エラー出ないし、新規開発や修正時のバグ出ないし、技術的負債がたまりにくいので競合他社は絶対に使わないでください。

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

TypeScript で書く React コンポーネントを基礎から理解する

この記事は Recruit Engineers Advent Calendar 2019 の 23日目の記事です。

TypeScript のプロジェクトで React コンポーネントを書いていると、コンパイラに怒られることがたびたびあります。ネット上にあるサンプルコンポーネントが JavaScript で書かれていると、プロジェクトにもってきたときにコンパイルできないということはよくあるんじゃないでしょうか。any でコンパイラを黙らせることもできますが、せっかく TypeScript を使っているので、安全に解決したいよねってことでこのような記事を書きました。

型定義は @types/react@16.9.17 にしたがっています。目次をみて、この程度のことはもう知っているよという方は react-typescript-cheatsheet をみてください。React + TypeScript のパターンがたくさんまとまっています。

基礎編

@types/react で用意されている型について説明します。なお、クラスコンポーネントについては説明しません。

関数コンポーネント (Function Components) の型

React.FC (React.FunctionComponent のショートハンド) という型が使えます。同じ型で React.SFCReact.StatelessComponent もありますが、こちらは現在非推奨です。

import React from "react";

type Props = {
  text: string;
}

const Test: React.FC<Props> = ({ children, text }) => (
  <div>
    <div>{text}</div>
    <div>{children}</div>
  </div>
);

props は React.FC<Props> のようにジェネリックで書くことができます。children は React.FC でコンポーネント定義すると暗黙的に使えるようになります。React.FC の型定義は以下のようになってます。

type FC<P = {}> = FunctionComponent<P>;

interface FunctionComponent<P = {}> {
    (props: PropsWithChildren<P>, context?: any): ReactElement | null;
    propTypes?: WeakValidationMap<P>;
    contextTypes?: ValidationMap<any>;
    defaultProps?: Partial<P>;
    displayName?: string;
}

type PropsWithChildren<P> = P & { children?: ReactNode };

関数コンポーネントの返り値は ReactElement または null となっています。

children の型

React.FC で定義される children はオプショナルであり、型は React.ReactNode です。props に children をあえて書くと以下のようになります。

type Props = {
  children?: React.ReactNode;
};

React.ReactNode は union 型で定義されています。

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

union されている型をさらに展開すると、

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;

ReactChildReactElementstringnumber の union 型です。ReactNode とあわせてみると、children として、stringnumberbooleannull、および undefined のプリミティブ型が渡せることがわかります。 ReactFragmentReactNode の配列型と定義されています。すると JSX のタグ構文で書くものは、ReactElement となります。説明を省略しましたが、ReactPortalPortals のことです。

props の型

children 以外の props は自由に設計することになりますが、基本は TypeScript の理解だけで十分です。

type Props = {
  str: string;
  num: number;
  bool: boolean;
  obj: {
    str: string;
  };
  strArr: string[];
  objArr: {
    str: string;
  }[];
  func: () => void;
}

React で用意されている便利な型もあります。CSS プロパティやイベント関連の型は以下のようになります。

type Props = {
  // css プロパティ
  style?: React.CSSProperties;
  // click イベントオブジェクトを引数で受け取る
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
  // onClick と同じ、handler の型も用意されている
  onClick2?: React.MouseEventHandler<HTMLButtonElement>;
}

マウスイベントだけでなく、ドラッグやクリップボードなど、イベント関連は一通り型定義が用意されています。

初級編

プロジェクトを進めていて実際にやってみたことを抜粋して紹介します。

children の型を制限する

基礎編で説明したように children の型は明示的に定義することができるので、children の型を制限することもできます。以下に例を示します。

// children を受け付けない
type Props = {
  children?: never;
};

// 文字列のみ
type Props = {
  children?: string;
};

// 単一要素のみ
type Props = {
  children?: React.ReactChild;
};

// 単一要素 (`ReactElement`) のみ
type Props = {
  children?: React.ReactElement;
};

余談ですが、React.ReactChild とタイピングすると、React.ReactChildren という型もエディタの補完ででてきます。名前が紛らわしいですが React.ReactChildrenchildren を処理するためのユーティリティであり、children の型指定で使うものではありません。

HTML 要素の props を参照する

HTML 要素を少しだけ拡張したいというときなどに使えるテクニックです。HTML 要素の props は JSX.IntrinsicElements["hoge"] で参照できます。 以下のコンポーネントでは、 button 要素の props がすべて受け取り可能になります。

type MyButtonProps = {
  // ここで拡張できる
} & JSX.IntrinsicElements["button"];

// type は button の props
const MyButton: React.FC<MyButtonProps> = ({ children, type }) => (
  <button type={type}>{children}</button>
);

TypeScript Playground

as props で render される要素を指定する

render される HTML 要素を動的に変えたいときに使います。styled-components などで使われているパターンです。

type Props = {
  as: React.ElementType<any>;
};

const Test: React.FC<Props> = ({ as: Component }) => (
  <Component />
);

const Parent = () => (
  <div>
    {/* a 要素になる */}
    <Test as="a" />
    {/* button 要素になる */}
    <Test as="button" />
  </div>
);

TypeScript Playground

React.ElementType という型を使っています。この型を使うことで HTML の要素名を props に渡すことができます。上記では any を指定していますが、特定の要素を指定することもできます。以下では button 要素と a 要素のみ指定できるようにしています。

type Props = {
  as: React.ElementType<JSX.IntrinsicElements["button"] | JSX.IntrinsicElements["a"]>;
};

TypeScript Playground

props を切り替える

受け付ける props のセットを切り替えることができます。<amp-img> のように layout 属性によって width / height 属性の有無が切り替わるときに使えるパターンです。<amp-img>layout="fixed" のときに widthheightlayout="fixed-height" のときに height のみを指定する必要があります。以下のように props を union 型で指定して、layout のガード節を入れることで props セットの切り替え再現することができます。

// width、height を定義
type FixedProps = { layout: "fixed"; width: number; height: number; };
// height のみ定義
type FixedHeightProps = { layout: "fixed-height"; height: number; };

const Image: React.FC<FixedProps | FixedHeightProps> = (props) => {
  if (props.layout ===  "fixed") {
    return <div>{props.layout}</div>;
  } else {
    // props.width // error
    return <div>{props.layout}</div>;
  }
}
const Parent = () => (
  <div>
    <Image layout="fixed" width={100} height={100} />
    <Image layout="fixed-height" height={100} />
    {/* <Image layout="fixed-height" width={100} height={100} /> // invalid */}
  </div>
);

TypeScript Playground

さいごに

基礎編で Event や Form や Hooks についても書きたかったけど、アドベントカレンダーに間に合わなかった。型安全なコンポーネント作りは、極めていくと無限にやりたい事がでてきますが、基礎編程度をおさえておけば自分で調べて解決できると思います。はじめに紹介しましたが、react-typescript-cheatsheet に多くのパターンが紹介されているので、困ったときにぜひ参考にしてみてください。

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

ReactでuseStateを上手く扱うにはどのように書くのがベストか

Hooks を使ってコードを書いていると、useState()の宣言が多くなってしまってごちゃごちゃしてしまうことがある。
このような問題に直面した時にどのように書いていくのが良いのだろうか。

単純に書いた場合、Class Component に慣れている方は、stateをまとめて宣言した方が見栄えが良いと思われるかもしれない。
これについてReactのドキュメントのFAQを読むと、次のように書いてある。

すべての state を 1 つの useState 呼び出しに含めても動作しますし、フィールドごとに別に useState を持たせることでも動作はします。しかしこれらの両極端の間でうまくバランスを取り、少数の独立した state 変数に関連する state をグループ化することで、コンポーネントは最も読みやすくなります。state のロジックが複雑になった場合は、それをリデューサで管理するか、カスタムフックを書くことをお勧めします。

つまり、基本的に少数の独立した state に分割して宣言することで、Custom Hook を作りやすくなり、リファクタリングしやすくなったり、コードが見やすくなるのである。

また、「どうにも上手くグループ分けができず、汚くなってしまった。」という時に限ってuseReducer()を使って、処理をまとめるのが良いと考える。

それ以外にもuseReducer()の利用場面として、以下のような場面がある。

そのため、次のようなステップでコードを書いていくのが良いと思われる。

  1. const x = useState(0);のように独立させて宣言してみる
  2. const [position, setPosition] = useState({ x: 0, y: 0});のようにして役割ごとにまとめてみる
  3. それでも上手くいかない場合は、useReducer()を使うというようにしたほうが共通化しやすいと考える。

メモ程度にまとめてみました!
間違っている所や他に意識しているようなことがあれば教えていただけたら嬉しいです。

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

ReactでReduxを使わずにFluxパターンを実装する

これはなに

新しめのReact (※) にはContextとHooksという2つのAPIが追加されており、これらを用いるとReduxで実装するようなFluxパターンを実現することが可能です。いつかくる最終戦争によって世界中のReduxが滅んでも、Reactさえ使えればクリーンな状態管理を続けることができるという訳ですね。

Reduxのサンプルでよくある絞り込み機能付きのToDoリストを用いて、storeを作ってblobal stateを共有しcomponentから利用するところまでを解説します。コードはGitHubにあります。

また、以下の本文中に添付するコードは可読性の都合で一部改変しているところがあります。ご了承ください。

※ - Context APIは v16.3.0、Hooks APIはv16.8.0 からです。

構成

src ディレクトリの中身はこんな感じです。

.
├── App.css
├── App.tsx
├── components
│   ├── TodoAdd
│   │   └── index.tsx
│   ├── TodoList
│   │   └── index.tsx
│   └── VisibilityFilterSelect
│       └── index.tsx
├── index.scss
├── index.tsx
├── react-app-env.d.ts
├── store
│   ├── index.tsx
│   ├── todos
│   │   ├── actions.ts
│   │   ├── index.tsx
│   │   ├── reducer.ts
│   │   └── types.ts
│   ├── visibilityFilter
│   │   ├── actions.ts
│   │   ├── index.tsx
│   │   ├── reducer.ts
│   │   └── types.ts
│   └── visibleTodos
│       └── index.tsx
└── types
    └── index.ts

ぱっと見た感じはReact-Reduxプロジェクトみのある構成ですね。

storeを作る

型を定義する

TypeScriptを使う場合のみですが、storeの型を定義しておきます。
todos の型はこんな感じです。

GitHub

src/store/todos/types.ts
export type TodosState = {
  id: number;
  text: string;
  completed: boolean;
}[];

export type TodosAction = {
  type: "ADD_TODO" | "TOGGLE_TODO";
  id?: number;
  text?: string;
};

export type TodosContextType = React.Context<{
  state: TodosState;
  dispatch: React.Dispatch<TodosAction>;
}>;

TodosStateTodosAction はReduxでも使うので「あーアレね」という感じだと思います。 TodosContextType はReduxでいう provider の中身に相当するもので、一旦 state をそれを変更するための dispatch が入る…ということだけ定義しておきます。

reducerを定義する

stateとactionの型が決まったので、Reducerを作っていきます。

GitHub

src/store/todos/reducer.ts
export default (
  state: TodosState,
  action: TodosAction
): TodosState => {
  switch (action.type) {
    case "ADD_TODO":
      return (action.text)
        ? [...state, {
          id: state.length,
          text: action.text,
          completed: false,
        }]
        : state;
    case "TOGGLE_TODO":
      return state.map(
        (item) => (item.id === action.id)
          ? { ...item, completed: !item.completed }
          : item
      );
    default:
      return state;
  }
};

まあ特に変なところはないですね。Reduxで使うReducerとなんら変わらないと思います。

actionsを定義する

Reducerに変更を通達するactionsを実装します。

GitHub

src/store/todos/actions.ts
export const addTodo = (text: string): TodosAction => ({
  type: "ADD_TODO",
  text,
})

export const toggleTodo = (id: number): TodosAction => ({
  type: "TOGGLE_TODO",
  id,
});

普通ですね。

ContextProviderを定義する

やってきました。本丸です。
Storeを作成し、その状態と変更関数を子componentに配布していきます。

GitHub

/store/todos/index.tsx
const initialState: TodosState = [];

export const TodosContext: TodosContextType = React.createContext<{
  state: TodosState;
  dispatch: React.Dispatch<TodosAction> | Noop;
}>({
  state: initialState,
  dispatch: () => {},
});

const TodosContextProvider: React.FC<React.Props<{}>> = ({
  children,
}): JSX.Element => {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  return (
    <TodosContext.Provider value={{ state, dispatch }}>
      {children}
    </TodosContext.Provider>
  );
};

export default TodosContextProvider;

なげえ。一気にややこしくなりました。
順番に解説します。

まずは Context API を使って子componentに渡すための Context を実装しています。
ちなみに引数に渡す値は初期値ではなく 子componentが読み込みを試みたとき該当するProviderが無かった際に使われる値 です。実質ほぼ使われないのですが、適当にnullとか入れると型解析の都合が悪くなることなどもあり、いちおう初期値いれとくか…というぐらいの気持ちに落ち着きました。

子componentから呼び出す際にも使用するので export を忘れないように気をつけます。

/store/todos/index.tsx
const initialState: TodosState = [];

export const TodosContext: TodosContextType = React.createContext<{
  state: TodosState;
  dispatch: React.Dispatch<TodosAction> | Noop;
}>({
  state: initialState,
  dispatch: () => {},
});

で、いま作った Context に渡すReducerを作ります。

useReducer はreducerと初期値を受け取って statedispatch を返します。Hooksを利用したことでこれらはこのcomponentのstateとして管理されるため、Context APIをよく考えずに使うと起きる「状態更新の度に再描画が走る」リスクは心配無用だと思います。

また、Context Providerは1つのstoreにつき1つ定義することになるのでどうやってもネスト地獄が発生します。変に結合させるとややこしくなるので children を受け取る形で実装しておき、後でまとめるとよいかと思います。

/store/todos/index.tsx
const TodosContextProvider: React.FC<React.Props<{}>> = ({
  children,
}): JSX.Element => {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  return (
    <TodosContext.Provider value={{ state, dispatch }}>
      {children}
    </TodosContext.Provider>
  );
};

export default TodosContextProvider;

storeを組み込む

いよいよアプリケーションにstoreを組み込んでゆきます。

GitHub

src/store/index.tsx
const ContextProvider: React.FC<React.Props<{}>> = ({
  children,
}): JSX.Element => (
  <VisibilityFilterContextProvider>
    <TodosContextProvider>
      <VisibleTodosContextProvider>
        {children}
      </VisibleTodosContextProvider>
    </TodosContextProvider>
  </VisibilityFilterContextProvider>
);

export default ContextProvider;

ネスト地獄です。こればかりは避けられませんが、逆にこういうAPI都合の謎構造は一箇所に固めて (ある程度) ブラックボックス化させておけるので、割り切ることもできるような気がしています。

特に言うことはないですが、後で解説するように「複数storeのstateを集約する」みたいなことをやろうとすると、ネストの順番が割と大事になってくるので注意が必要です。

最後にこれらをアプリケーションに接続します。 App.tsx 的な、ルート階層に近い部分で行います。このサンプルだと src 直下ですね。

GitHub

src/index.tsx
const App: React.FC = (): JSX.Element => (
  <ContextProvider>
    <div className="container">
      <TodoAdd />
      <VisibilityFilterSelect />
      <TodoList />
    </div>
  </ContextProvider>
);

ReactDOM.render(<App />, document.getElementById('root'));

これでアプリケーションとstoreがつながりました。
いよいよ状態の読み出しと更新を行なっていきます。

storeを使う

ToDoリストへの項目追加を行う TodoAdd コンポーネントを例に解説します。

GitHub

src/components/TodoAdd/index.tsx
const TodoAdd: React.FC = (): JSX.Element => {
  const { state, dispatch } = React.useContext(TodosContext);
  const [ text, setText ] = React.useState("");
  const handleClick = () => {
    if (!dispatch) return;
    dispatch(addTodo(text));
    setText("");
  }
  return (
    <form className="TodoAdd">
      <input
        type="text"
        value={text}
        onChange={e => setText(e.currentTarget.value)}
        className="TodoAdd__input"
      />
      <button onClick={handleClick} disabled={text === ""} className="TodoAdd__button">追加</button>
    </form>
  )
}

export default TodoAdd;

大事なのは useContext を利用してContextから値を取り出し、そのまま利用できるという点ですね。

src/components/TodoAdd/index.tsx
// ↓ここでContextを読み込んで
import { TodosContext } from "../../store/todos";

const TodoAdd: React.FC = (): JSX.Element => {
  // ↓ ここで値を取り出している!
  const { state, dispatch } = React.useContext(TodosContext);
  const [ text, setText ] = React.useState("");
  const handleClick = () => {
    if (!dispatch) return;
    // ↓ アクションをdispatchして状態を更新できる!
    dispatch(addTodo(text));
    setText("");
  }
  // ...

いいですね〜Reduxの connect に比べてシンプルな印象がないでしょうか。そんなことないですか。

おまけ

combinated storeを作る

2つ以上のstoreを結合したいときには単純にcontextをたくさん読み込んで計算した結果を新たなcontextとして配布すればよいのではないかと思います。

以下は visibilityFilter (表示するToDo項目の絞り込み) と todos (ToDo全項目) を使って「表示するToDo項目」を作っているものです。

GitHub

src/store/visibleTodos/index.tsx
export const visibleTodosContext = React.createContext<{
  state: TodosState;
}>({
  state: initialState,
});

const VisibleTodosContextProvider: React.FC<React.Props<{}>> = ({
  children,
}): JSX.Element => {
  const { state: todosState } = React.useContext(TodosContext);
  const { state: visibilityFilterState } = React.useContext(VisibilityFilterContext);
  const stateFactory = (todos: TodosState): TodosState => {
    switch(visibilityFilterState) {
      case "SHOW_ALL":
        return todos;
      case "SHOW_ACTIVE":
        return todos.filter(todo => !todo.completed)
      case "SHOW_COMPLETED":
        return todos.filter(todo => todo.completed)
      default:
        return todos;
    }
  }

  return (
    <visibleTodosContext.Provider value={{ state: stateFactory(todosState) }}>
      {children}
    </visibleTodosContext.Provider>
  )
}

export default VisibleTodosContextProvider;

前述しましたが、 Context APIのProviderは子componentにしか状態を伝播させない ので、これを ContextProvider のルート階層に持っていくとエラーになります。
ちょっと気持ち悪さがありますが、やむを得ない感じもあります。

課題など

今回は使いませんでしたが、ほとんどのアプリケーションではAPIへのアクセスなど非同期処理が発生すると思います。今の所Reducer Hookにはmiddlewareの利用にデファクト的なものがないっぽいので、どうハンドリングするかは少し考える必要があります。同期的に処理の開始と完了で2つactionを発行するという手もあるし、自前でmiddle的なものを実装する という手もあるでしょう。

まとめ

これでRedux封じに遭っても安心ですね。
今はまだ「使えんこともない」という感じですが、こうしたReact本来のAPIを活かしつつ拡張していくようなエコシステムが成熟してくると大分可能性はあるな…と感じます。以上です。

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

Reactで親から子のメソッドを実行するサンプル

はじめに

Reactにおいてコンポーネント間のメソッド実行はReduxなど使ってやるのが一般的ですが、
さくっとつくりたいアプリケーションにReduxは荷が重いです。

また、子が親のメソッドを実行するのはpropsで受け取った関数を呼べば良いだけですが、
親が子のメソッドを実行するのは意外と面倒みたいです。(なかなか調べても出てこない)

サンプル

今回のサンプルはuseRef, useImperativeHandle, forwardRefを使って子メソッドの実行を実現しています。
codesandboxにコピペしてmaterial-uiをADDすればすぐ動きます

import React, { useState, useRef, useImperativeHandle, forwardRef } from "react";
import Button from "@material-ui/core/Button";

export default function Parent() {
  const parentAdd = () => {
    Children.current.plus()
    console.log('親です')
  }
  return (
    <div>
      <Children parentAdd={parentAdd} ref={Children}/>
      <Button onClick={parentAdd}>親add</Button>
    </div>
  );
}

let Children = (props, ref) => {
  const [state, setState] = useState(1);
  const inputRef = useRef()
useImperativeHandle(ref, () => ({
  plus: () => {
    plus()
  }

}))

  const plus = () => {
    setState(state + 1);
  };
  return (
    <div ref={inputRef}>
      <div>{state}</div>
      <Button onClick={plus}>子add</Button>
    </div>
  );
};
Children = forwardRef(Children)

動き

親と子どちらのボタンを押しても数字が反応します。

親の持ち物

 ・子のplus()を実行する「親addボタン」←これがやりたいこと
 ・子

子の持ち物

 ・数字の描画
 ・数字をインクリメントする関数plus()を実行する「子addボタン」

注意

コンソールに以下のWarningが出るのでご注意ください。

Warning: Expected useImperativeHandle() first argument to either be a ref callback or React.createRef() object. Instead received: an object with keys {$$typeof, render}.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React 状態によってデザインの変わるボタンコンポーネント作成(CSS Module, classnames)

:calendar_spiral: i-plug Advent Calendar 2019 の【19日目】の記事です:santa::tada:

はじめまして、i-0701 です:boy::boy_tone4::boy_tone1::boy_tone2::boy_tone3:

この度 React を触ることになりまして、まずはボタンのコンポーネントを作成しよう!と思ったので、その備忘録を残します。
私が css まわりから作成していくタイプのため、今回は style 表示切り替え特化 です。(イベント処理に関しては今回書いておりません:cry:

ちなみに使うものは
- CSS Module
- classnames

CSS Module ?
css で名前空間設定できるようなやつです。スコープしぼって使えるやつです。
1 個の css 修正してあそこもここも影響でた!!がなくて私は好きです。
共通なデザインの調整 css いっぱいやと、複数箇所修正する懸念がないこともないですが、コンポーネントなどはそれ特化の css するので問題ないはず。(最終、ここの margin は共通!って場合、sass で変数持ったらいいかもしれない)
あと、class名を考える時間が削減するので好きなのもあります。

create-react-app

node 動く状態で(私の環境は v10.13.0)、create-react-app します。
今回は使いませんが、typescript 好きなので入れてます。

$ npx create-react-app ugoke --typescript
# npx create-react-app my-app --template typescript のが今正しかった

create-react-app した直後だと、webpack の設定だとかが見れないので、npm run eject 実行。

$ cd ugoke
$ npm run eject

コンポーネントと style.module.scss つくる

src 以下に Button コンポーネントを作成。
scss 動かしたいので、node-sass いれます。

$ npm install node-sass

あとは .module.scss で作成すると勝手に create-react-app さんがええ感じにしてくれます。(CSS modules とかのモジュール入れなくても動く)

src
├── App.css
├── App.test.tsx
├── App.tsx
└── components 
  └── Button
    ├── Button.tsx 
    └── style.module.scss ← .module.scss にすること!
Button.tsx
import React from 'react';
import styles from './style.module.scss';

// ちょっとの typescript 要素
interface Button {
  text: string,
}

function Button(props: Button) {
  return (
    <button className={styles.button} onClick={props.event}>
      {props.text}
    </button>
  );
}

export default Button;
style.module.scss
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center; // color とか変数として持つほうがいいけど今回はしないです。
  font-size: 14px;
  background: #F1F1F1;
  color: #333;
  border-radius: 4px;
  border: 1px solid #BFBFBF;
  padding: 6px 12px;
  cursor: pointer;
}
App.tsx
import React from 'react';
import './App.css';
import Button from './components/Button/Button';

const App: React.FC = () => {
  return (
    <div className="App">
      <Button text="ボタン" />
    </div>
  );
}

export default App;

表示されました:blush:
スクリーンショット 2019-12-13 16.24.44.png

ちなみに

あとは .module.scss で作成すると勝手に create-react-app さんがええ感じにしてくれます。(CSS modules とかのモジュール入れなくても動く)

npm run eject してでてきた config/webpack.config.js を見ると、サポートしてるよって書いてくれてます。
今回だとの 457~490 行目あたりです。create-react-appさまさま:pray:

config/webpack.config.js
            // Opt-in support for SASS (using .scss or .sass extensions).
            // By default we support SASS Modules with the
            // extensions .module.scss or .module.sass
            {
              test: sassRegex,
              exclude: sassModuleRegex,
              use: getStyleLoaders(
                {

props によって style を変更する

今回は props によって見た目を disabled できるようにしてみます。

まずは複数の class 扱うのに便利な classnames を入れます。

$ npm install classnames --save
# 「型がない」って怒られるので入れます
$ npm install @types/classnames

CSS Module を使ってるのでこちら参照してコンポーネントを修正

Button.tsx
import classNames from 'classnames/bind';

interface Button {
  text: string,
  disabled?: boolean,
}

let cx = classNames.bind(styles);

function Button(props: Button) {
  let buttonClass = cx({
    button: true,  // ベースの class
    disabled: props.disabled,
  });

  return (
    <button className={buttonClass}>
      {props.text}
    </button>
  );
}

disabled 用の class を書きます。

style.module.scss
.button {

  // 略

  padding: 6px 12px;
  cursor: pointer;

  &.disabled {
    cursor: default;
    opacity: .65;
  }
}

App で呼びます。

App.tsx
const App: React.FC = () => {
  return (
    <div className="App">
      <Button text="ボタン" />
      <Button text="ボタン" disabled={true} />
    </div>
  );
}

表示されました:blush:
左が通常ボタン、右が disabled なボタンです。
スクリーンショット 2019-12-18 18.43.24.png

おまけ :yum: 失敗コードと感想

最初 CSS Module で以下みたいな書き方が分からず、

  var btnClass = classNames({
    btn: true,
    'btn-pressed': this.state.isPressed,
    'btn-over': !this.state.isPressed && this.state.isHovered
  });

  // これだと動かない
  var btnClass = classNames({
    'styles.button': true,
    'styles.disabled': props.disabled,
  });

こんな↓書き方してしまったんですが……公式を読むとちゃんと書いてました:bow_tone2:

  const classNameForButton = () => {
    if (props.disabled) {
      return ClassNames(styles.button, styles.disabled);
    }
    return ClassNames(styles.button);
  };



公式 Docs をちゃんと読もうって思いました:bangbang::bangbang::bangbang:

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

【業務】『何使ってもいいよ』と言われたプロジェクトにReactを導入して感じた事【利用経験者ゼロ】

Reactというフレームワークに魅入られ『いつか業務で使ってみたい!』と焦がれ幾年。
巡り巡ってようやくReact開発のプロジェクトに出会えた際に感じた事を、記事にまとめようと思います。

筆者について

  • Webエンジニア
  • 業務ではjQueryでDOM操作ゴリゴリというプロジェクトを転々
  • React自体は未経験ではなかった
    • v14辺りから個人的な学習は重ねていた
      • 業務での利用経験はゼロ タイトル詐欺??

プロジェクトについて

  • (当然)Webアプリケーション開発のプロジェクト
    • バックエンドはPHP
    • 業務系のBtoBなアプリケーション(not SPA)
  • 他メンバーは筆者と違い、ReactやモダンなJSフレームワーク自体が完全に初体験

経緯

  • 参画当初、具体的なフロントエンドのアーキテクチャは決まっていなかった。
  • 『jQueryで作ろうかなー、なんか他にイイのある?』くらいの温度感
  • Reactを提案してみる → あっさり『イイね!みんなで勉強しながら使ってみよう!』 となる(許可、検討頂いた関係各位には感謝しかありませんm(_ _)m)

導入してメリットに感じた点

state管理の素晴しさ

DOMの操作は必ずstateの変更を経なければいけない、というReactの制約はjQueryに慣れた身には窮屈に感じる場面が多々ありました。
しかしその窮屈さを乗り越えた先にあったものは、jQueryやテンプレートエンジンでは得難い、動的なWEBアプリとしての『堅牢さ』でした。

どんなWebアプリも動的である以上、状態を持つ事は免れません。

  • 検索結果の一覧表示 → 『検索結果という状態』によって増減する一覧の表示
  • 管理者権限でのみ表示されるボタン → 『権限という状態』次第で表示されるボタン

ReactやVueでは動的項目を全て状態(state)とみなし、状態の変化をトリガーにし、変化した状態を元に自動でDOMを組み替えていきます。
『状態変化後に、別途DOMを組み換えるロジック』を自前で実装する必要がありません。

function Example(props) {
  const [list, setList] = useState([]) // 初期値は空配列
  const [isAdministrator, setIsAdministrator] = useState(false) // 初期値はfalse

  // 『setList』と『setIsAdministrator』に値をセットするだけで、下記のDOMが自動で書き変わる

  return (
   <div>
    {* データ一覧表示 *}
    <table>
     <thead>
      <th>ID</th>
      <th>名前</th>
     <thead>
     <tbody>
      {list.map(item => (
        <tr>
         <td>{item.id}</td>
         <td>{item.name}</td>
        </tr>
      ))}
     </tbody>
    </table>

    {* 管理者権限のみ表示されるボタン *}
    {isAdministrator ? 
      (<button>承認</button>) : 
      (null)
     }
   </div>
  )
}

画面をフルレンダリングさせた上での動的な表示制御であれば、jQueryやテンプレートエンジンのみでも問題無く実現出来ますが、非同期通信を用いた動的な表示制御となると一変して、いちいちフラグ格納用の隠しDOMを用意したり、作りにもよりますがサーバーサイドでセッション領域にフラグとなる値をフラッシュしたりしなければならず、更に状態の変化とワンセットで、DOMの組み換え処理を自前で必ず『忘れずに』実装しなければなりません。

jQuery例 一覧表示処理DOM組み換え html
<!-- こんなDOMを用意しておいて・・・ -->
<table id="hogeTable">
 <thead>
  <th>ID</th>
  <th>名前</>
 </thead>
</table>
jQuery例 一覧表示処理DOM組み換え javascript
var tbody = $('tbody')

//『list』変数には非同期処理等で取得したデータの配列が格納されている
$.each(list, function(index, item) { 
  tbody.append(
    $('<tr>').append(
      $('<td>').text(item.id),
      $('<td>').text(item.name)
    )
  )
})

$('table#hogeTable').append(
  tbody
)

// 手続き的で、これだけの構成でも実装が結構しんどい…
//   →DOMの構成が少しでも変わると大事

状態を持ったアプリを作る以上、ReactやVueの様な状態管理の仕組みが無ければ、要件が変わって状態が1つ2つと増えていくうちに【条件判定 & DOM組替】のロジックがどんどん膨らんでいき、バグのリスクが急激に跳ね上がってあっという間に脆弱なアプリが出来上がってしまいます。

シェアが多いので文献が豊富

シェアが少ないフレームワークやライブラリは、ググってもまともに情報が出てこない、というケースが多々あります。
公式のリファレンスもあまりメンテナンスされておらず、いまいち不親切な書きっぷりの物も多いです。

ReactやVueであればシェアが非常に多い分、調査の結果解決策にたどり着きやすく、何より公式のリファレンスが非常に充実しており、技術的な問題に直面した際の安心感が段違いでした。

チーム全体におけるソースコードの品質が同じ水準になっていった

state管理から始まり、Reactは厳しさとも取れる制約を強いてくるフレームワークです。

  • DOM操作は絶対に状態(state)の変更を経る
  • DOM操作ではない、DOM『参照』においても、必ずstateを参照しなければダメ
    • const hoge = documen.getElementById('hoge').value; // ←ダメ
  • マークアップの閉じ忘れは絶対にNG(トランスパイルが通らなくなる。<br>とかもダメ、絶対閉じる → <br/>)
      ...等々

しかし、それらの厳格さの元に出来上がったソースは、チーム全体で似たような書きっぷりになっていき、気づけばスキルもキャリアも全く違う各員が、同じ目線、同じ思想を持ってソースコードが書ける様になっていきました。

楽しい

個人的にはこれが一番のメリットに感じました。

チームには、私より経験豊富なベテランPG、今回が初現場という新人PG等、幅広いキャリアのメンバーが揃っていましたが、Reactの基礎的な仕組みを理解頂いてからは『state変更を通じて、DOMをコントロールしている感じが面白い』『Reactを使って綺麗にソースを書いていく事が楽しい』という声が上がって来る様になりました。

メンバー全員にReactの思想の素晴らしい部分や、何よりReactを用いたプログラミングの楽しさを伝える事が出来て、本当に嬉しく思いました。
『楽しさ』はそのままパフォーマンスの向上に繋がり、作業効率や出来上がった機能の品質にそのまま直結していきました。

導入してデメリットに感じた点

学習コスト

従来の手続き的なDOM操作から、Reactの宣言的なDOM操作というのはかなりのパラダイムシフトで、私自身も過去に詰まった点等は、極力わかりやすくメンバーにお伝えしたつもりでしたが、それでもやはり導入して2~3日はまともにパフォーマンスが発揮できたメンバーはいないという状態でした。

ですが、前述のメリットの通りその分も遅れも十分ペイ出来るのパフォーマンスを、最終的にはチーム全体で発揮できたので、とりあえずは結果オーライだったのかな、と思っています。

ブラウザバックの制御が(若干)面倒

開発するWebアプリにおいては、ブラウザバックが正しく動作する、という要件は満たす必要がありました。

私自身は過去にreact-routerを利用して、学習がてら簡素なアプリのブラウザバックの機構を実装した事はあったのですが、業務での利用は初めてだったので、要件通りに機能を満たせるか、不安に感じていた点ではありました。

結果react-router並びに、react-routerが提供してくれているhooks API(v4~)を利用する事により、旧来のバージョンのreact-routerに比べて、更に簡潔な記述でブラウザバック動作の処理を実現させる事が出来ました。

しかしブラウザバックを検知するhistoryによるlisten周りの処理は、コンポーネント内に副作用として記述せざるを得なかったので、やはりSPAフレームワークによるブラウザバックの制御はしんどいものがあるな、と改めて実感しました。

まとめ

ReactはSPAを作成するフレームワークである、という認識も根強いかと思いますが、state管理というパラダイムはSPAに限らず、『多様な状態』を持つ業務系のアプリケーションにおいても十分に有用であると感じました。

jQueryやテンプレートエンジンが一様に劣っている、という訳ではありませんが、ReactやVueが持つ技術的なパラダイムは、jQueryや旧来のテンプレートエンジンでは応えきれなくって来た、昨今のユーザーがWebアプリに求める多様な要件に対する解の一つである、と感じました。
 

さぁ、業務でjQueryしか触ったことが無いというアナタも
今すぐ環境構築をしてLet's Reactライフ!

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

Reactで(少し)動的なFormを作る

やりたいこと

  • 上段のプルダウンで値を選ぶと、下段のプルダウンで選べる要素が変わる(制限される)
  • ModalでFormを出して値を任意の数追加する

メモ

  • なぜかFormikのvalidateField()の実行がエラーとなる(issue上がってない・・・)

完成イメージ

下記のような感じ。エリアで関西とかを選ぶと、大阪、兵庫とかしか選べなくなる。
趣味はいくつあるかわからないので、必要数入れてもらう(うらでは配列とかで持つ)。
コードはgithubにあげてみたので、動作確認してからいじったほうがわかりやすいかも。

スクリーンショット 2019-12-19 7.11.12.png

実装

正解があるわけではないが、とりあえず下記のようにしてみた。

App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';

import { Formik, Form as FormikForm, validateYupSchema } from 'formik';
import * as Yup from 'yup';
import {
    Form,
    FormGroup,
    Label,
    Input,
    FormFeedback,
    Button,
    Modal,
    ModalHeader,
    ModalBody,
    ModalFooter
} from 'reactstrap';


const areas = [
    { name: '選択してください', value: '選択してください' },
    { name: '関西', value: '関西' },
    { name: '関東', value: '関東' },
    { name: 'その他', value: 'その他' },
];

const prefs = [
    { area: '選択してください', name: '選択してください', value: '選択してください' },
    { area: '関東', name: '東京', value: '東京' },
    { area: '関東', name: '千葉', value: '千葉' },
    { area: '関西', name: '大阪', value: '大阪' },
    { area: '関西', name: '兵庫', value: '兵庫' },
    { area: 'その他', name: '北海道', value: '北海道' },
]

class App extends React.Component {

    state = {
        modalOpen: false,
        hobbies: [],
    }

    handleSubmit1 = (values) => {
        alert(JSON.stringify(values));
    }

    handleSubmit2 = (values) => {
        alert(JSON.stringify(values));
    }

    handleHobbyAdd = async (hobby) => {
        const _hobbies = [...this.state.hobbies];
        _hobbies.push(hobby);
        await this.setState({ hobbies: _hobbies });
        this.handleModalClose();
    }

    handleModalClose = () => {
        this.setState({
            modalOpen: false,
        });
    }

    handleModalOpen = () => {
        this.setState({
            modalOpen: true,
        });
    }

    handleHobbyDelete = (index) => {
        // alert(index);
        const _hobbies = [...this.state.hobbies];
        _hobbies.splice(index, 1);
        this.setState({ hobbies: _hobbies });
    }

    render() {
        return (
            <div className="container">
                <h3 className="my-5 text-center">動的フォームテスト</h3>
                <h4 className="my-4">エリア選択フォーム</h4>
                <Formik
                    initialValues={{ area: '選択してください', pref: '選択してください' }}
                    onSubmit={(values) => this.handleSubmit1(values)}
                    validationSchema={Yup.object().shape({
                        area: Yup.string().notOneOf(['選択してください']),
                        pref: Yup.string().notOneOf(['選択してください']),
                    })}
                >
                    {
                        ({ handleSubmit, handleChange, handleBlur, values, errors, touched }) => (
                            <Form onSubmit={handleSubmit}>
                                <FormGroup>
                                    <Label>エリア</Label>
                                    <Input
                                        type="select"
                                        name="area"
                                        value={values.area}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.area && errors.area)}
                                    >
                                        {
                                            areas.map((area, index) => (
                                                <option value={area.value} key={index}>{area.name}</option>
                                            ))
                                        }
                                    </Input>
                                    <FormFeedback>{errors.area}</FormFeedback>
                                </FormGroup>
                                <FormGroup>
                                    <Label>都道府県</Label>
                                    <Input
                                        type="select"
                                        name="pref"
                                        value={values.pref}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.pref && errors.pref)}
                                    >
                                        {
                                            (prefs.filter(pref => pref.area === values.area || pref.area === '選択してください')).map((pref, index) => (
                                                <option value={pref.value} key={index}>{pref.name}</option>
                                            ))
                                        }
                                    </Input>
                                    <FormFeedback>{errors.pref}</FormFeedback>
                                </FormGroup>
                                <div>
                                    <Button type="submit" color="primary">登録</Button>
                                </div>
                            </Form>
                        )
                    }
                </Formik>
                <h4 className="my-4">趣味登録フォーム</h4>
                <Formik
                    enableReinitialize
                    initialValues={{ name: 'hoge', hobby: '', hobbies: this.state.hobbies }}
                    onSubmit={values => this.handleSubmit2(values)}
                    validationSchema={Yup.object().shape({
                        name: Yup.string().required(),
                        hobbies: Yup.array().min(1),
                    })}
                >
                    {
                        ({ handleSubmit, handleBlur, handleChange, values, errors, touched, validateField, setFieldValue }) => (
                            <Form onSubmit={handleSubmit}>
                                <FormGroup>
                                    <label>氏名</label>
                                    <Input
                                        type="text"
                                        name="name"
                                        value={values.name}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.name && errors.name)}
                                    />
                                    <FormFeedback>
                                        {errors.name}
                                    </FormFeedback>
                                </FormGroup>
                                <FormGroup>
                                    <legend className="col-form-label">趣味</legend>
                                    <div>
                                        <Button type="button" size="sm" color="success" onClick={() => {
                                            // setFieldValue('hobby', '');
                                            this.handleModalOpen();
                                        }}>趣味を追加 +</Button>
                                        {
                                            this.state.hobbies.map((hobby, index) => (
                                                <div className="row container" key={index}>
                                                    <Input
                                                        type="text"
                                                        value={hobby}
                                                        disabled
                                                        className="col-11 my-3"
                                                    />
                                                    <Button
                                                        className="col-1 my-3 px-1"
                                                        type="button"
                                                        onClick={() => this.handleHobbyDelete(index)}
                                                    >X</Button>
                                                </div>
                                            ))
                                        }
                                    </div>
                                    <p className="text-danger"><small>{touched.hobbies && errors.hobbies ? errors.hobbies : null}</small></p>
                                </FormGroup>

                                <Modal isOpen={this.state.modalOpen}>
                                    <ModalHeader toggle={this.handleModalClose}>趣味</ModalHeader>
                                    <ModalBody>
                                        <FormGroup>
                                            <Label>趣味名</Label>
                                            <Input
                                                type="text"
                                                name="hobby"
                                                value={values.hobby}
                                                onChange={handleChange}
                                                onBlur={handleBlur}
                                                invalid={Boolean(touched.hobby && errors.hobby)}
                                            />
                                            <FormFeedback>{errors.hobby}</FormFeedback>
                                        </FormGroup>
                                    </ModalBody>
                                    <ModalFooter>
                                        <Button type="button" color="info" onClick={() => {
                                            // validateField('name');
                                            // validateField('hobby');
                                            // なぜかvalidateFieldがエラーになる。。。
                                            if (values.hobby === '') {
                                                alert("必須です。")
                                                return;
                                            }
                                            this.handleHobbyAdd(values.hobby);
                                        }}>登録</Button>
                                        <Button type="button" color="secondary" onClick={this.handleModalClose}>キャンセル</Button>
                                    </ModalFooter>
                                </Modal>

                                <div>
                                    <Button type="submit" type="submit" color="primary">登録</Button>
                                </div>
                            </Form>
                        )
                    }
                </Formik>
            </div>
        );
    }
}

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

Vueを半年弱触ってた人がReactに手を出して一ヶ月目の所感

この記事の目的

Vue経験者でReactに興味ある人が"やるorやらない"を決めるキッカケになればいいかなと思います。

自己紹介

情報専門学生3年のkazzです。

学校とはまた別で、Web開発のデザイン以外を手広く触っています。最近はもっぱらSPAつくってます(バックNodejs,フロントReact,インフラAWS/EC2)。

ちなみに自分は、ReactもVueも個人制作やハッカソンで使う程度で実務では使ったことがないですし、そこまで精通しているわけでもないので、本記事は「そういうもんなんだな〜」程度で見てくれるとありがたいです。

Reactに移行してみて

移行した経緯

正直、当時のVueの選択理由も学習コストの低さと情報の多さくらいで「まあVueでいいか」みたいな感じだったので、Vueを書き始めて半年弱でやはりReactやAngulerに興味が湧いてきてしまい、JavaScriptもそこそこ書けるようになってきたのもあって、Reactに移行することにしました。

そうです、一番は知的好奇心です。

Reactを選んだ理由

Angulerは大規模開発向きな分ニーズが偏っているそうで、現在の案件数も一番多く将来性もありそうなReactを選択しました。

それに、Angulerは小中規模のアプリを開発するにしてはオーバーヘッドが大きく向いていないそうですし、自分的には違うかなと感じました。

VueからReactに移行して良かったこと、悪かったこと

これは、正直まだ分かりません...。というか、これは設計思想やチーム開発などで得られる恩恵が大きいような気もするので、この段階で分からないのは当然なのかもしれません。

実際Vueのときと比べ、Reactだから出来たとか出来ないだとかは特にありませんし、どちらも日本語ドキュメントが用意されていますし(※19年12月時点でReactは一部和訳されていない部分があるが、そんなに支障はなかった)、ライブラリも豊富に存在します。

難易度、学習コスト

現段階では、どちらもさほど変わらないかなという印象です。

ただ、VueのSFCのような直感的な分かりやすさはなく、少し慣れが必要かなと感じました。

あと、Vue以上にES6の理解は必要です。公式のチュートリアルにも「ES6 という JavaScript の最近のバージョンからいくつかの機能を使用していることにも注意してください」と書かれています。

最低でもconst,letや分割代入、スプレッド演算子、アロー関数くらいはやっておくとスムーズに学習できると思います。

実際に触ってみよう

環境構築(Vue環境有前提)

Nodejsの環境は出来上がっていると思うので、さくっとプロジェクトを作成しましょう。

まず、npmを使わずnpxコマンドからcreate-react-appを実行し、プロジェクトの雛形を作成します。

npx create-react-app sample-app

こんな構成で出来上がると思います。
Screenshot from 2019-12-19 03-19-44.png

とりあえずnpm run startでReactアプリケーションを起動してみましょう。
Screenshot from 2019-12-19 03-34-21.png

さらに、ディレクトリを少し整頓して、src/App.jsを最小構成にしてみましょう。
Screenshot from 2019-12-19 03-53-09.png

結果
Screenshot from 2019-12-19 03-54-26.png

Vueとの比較

せっかくなのでReactの一部機能をVueと比較しながら紹介します。

SFCとJSX

ReactではJSXという記法を使いHTMLを表現します。Vueでいう単一ファイルコンポーネントのtemplateみたいな感じです。

JSX構文を使えばHTMLをJavaScriptにそのまま書くことができ、更には変数に代入し利用することも可能です。

Vueの場合

App.vue
<template>
    <div id="App">
        Hello, OthloTech!
    </div>
</template>

<script>
export default {
    name: "app"
};
</script>

<style>
.App {
    margin: 200px;
}
</style>

Reactの場合

App.js
import React from 'react';
import "./App.css"; // <- CSSファイル適用

const App = () => {
    return (
        <div className="App">
            Hello, OthloTech!
        </div>
    );
}
export default App;
App.css
.App {
    margin: 200px;
}

下は少しお待ちを...
いいねが付けば付くほど急ぎます

  • ### StateとProps
  • ### Router

学習方法

UdemyのReactレッスンでざっくり理解

本や検索で知識を固めつつコーディング

Reactの公式ドキュメントを見ながらリファクタリング

React公式ドキュメント:
https://ja.reactjs.org/docs/getting-started.html

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

Vueを半年弱触ってた人がReactに移行して一ヶ月目の所感

この記事の目的

Vue経験者でReactに興味ある人が"やるorやらない"を決めるキッカケになればいいかなと思います。

自己紹介

情報専門学生3年のkazzです。

学校とはまた別で、Web開発のデザイン以外を手広く触っています。最近はもっぱらSPAつくってます(バックNodejs,フロントReact,インフラAWS/EC2)。

ちなみに自分は、ReactもVueも個人制作やハッカソンで使う程度で実務では使ったことがないですし、そこまで精通しているわけでもないので、本記事は「そういうもんなんだな〜」程度で見てくれるとありがたいです。

Reactに移行してみて

移行した経緯

正直、当時のVueの選択理由も学習コストの低さと情報の多さくらいで「まあVueでいいか」みたいな感じだったので、Vueを書き始めて半年弱でやはりReactやAngulerに興味が湧いてきてしまい、JavaScriptもそこそこ書けるようになってきたのもあって、Reactに移行することにしました。

そうです、一番は知的好奇心です。

Reactを選んだ理由

Angulerは大規模開発向きな分ニーズが偏っているそうで、現在の案件数も一番多く将来性もありそうなReactを選択しました。

それに、Angulerは小中規模のアプリを開発するにしてはオーバーヘッドが大きく向いていないそうですし、自分的には違うかなと感じました。

VueからReactに移行して良かったこと、悪かったこと

Vueだとtemplateが制御構文などで肥大化してネストが凄いことになることがありましたが、Reactはわざわざ別ファイル化せずともコンポーネントの一部を抽象化して外にかけたり、jsの制御構文をそのまま使えたりと、汎用性の高さを感じました。

参考: 条件付きレンダー

実際Vueのときと比べ、Reactだから出来たとか出来ないだとかは特にありませんし、どちらも日本語ドキュメントが用意されていますし(※19年12月時点でReactは一部和訳されていない部分があるが、そんなに支障はなかった)、ライブラリも豊富に存在します。

難易度、学習コスト

現段階では、どちらもさほど変わらないかなという印象です。

ただ、VueのSFCのような直感的な分かりやすさはなく、少し慣れが必要かなと感じました。

あと、Vue以上にES6の理解は必要です。公式のチュートリアルにも「ES6 という JavaScript の最近のバージョンからいくつかの機能を使用していることにも注意してください」と書かれています。

最低でもconst,letや分割代入、スプレッド演算子、アロー関数くらいはやっておくとスムーズに学習できると思います。

実際に触ってみよう

環境構築(Vue環境有前提)

Nodejsの環境は出来上がっていると思うので、さくっとプロジェクトを作成しましょう。

まず、npmを使わずnpxコマンドからcreate-react-appを実行し、プロジェクトの雛形を作成します。

npx create-react-app sample-app
# TypeScriptを導入する場合は↓で実行
# npx create-react-app sample-app --typescript

こんな構成で出来上がると思います。
Screenshot from 2019-12-19 03-19-44.png

とりあえずnpm run startでReactアプリケーションを起動してみましょう。
Screenshot from 2019-12-19 03-34-21.png

さらに、ディレクトリを少し整頓して、src/App.jsを最小構成にしてみましょう。
Screenshot from 2019-12-19 03-53-09.png

結果
Screenshot from 2019-12-19 03-54-26.png

Vueとの比較

せっかくなのでReactの一部機能をVueと比較しながら紹介します。

Template→JSX

ReactではJSXという記法を使いHTMLを表現します。Vueでいう単一ファイルコンポーネントのtemplateみたいな感じです。

JSX構文を使えばHTMLをJavaScriptにそのまま書くことができ、更には変数に代入し利用することも可能です。View内で変数を使い場合は{}で囲めばその中は式として判定されます。

Vueの場合

App.vue
<template>
    <div class="App">
        Hello, OthloTech!
    </div>
</template>

<script>
export default { };
</script>

<style scoped>
.App {
    margin: 200px;
}
</style>

Reactの場合

App.js
import React from 'react';
import "./App.css"; // <- CSSファイル適用

const App = () => {
    return (
        <div className="App">
            Hello, OthloTech!
        </div>
    );
}
export default App;
App.css
.App {
    margin: 200px;
}

State

VueでいうdataがReactのStateにあたります。コンポーネント内の情報で保持する変数です。

Vueはdataの中身を直接再代入すればViewにも反映されますが、Reactはそうはいきません。State定義時にあわせてセッター関数も定義し、そのセッター経由でデータの変更を行います。

次の例はよくあるカウントアップアプリを実装しています。

Vueの場合

App.vue
<template>
    <div>
        <div>{{count}}</div>
        <button @click="count += 1">PLUS</button>
    </div>
</template>

<script>
export default {
    data: () => ({
        count: 0
    })
};
</script>

Reactの場合

App.js
import React from 'react';

const App = () => {
    //    getter   setter                  初期値
    //      ↓        ↓                       ↓
    const [count, setCount] = React.useState(0);

    return (
        <div>
            <div>{count}</div>
            <button onClick={() => { setCount(count + 1) }}>PLUS</button>
        </div>
    );
}

export default App;

Props

おなじみpropsです。

Vueの場合

SampleComponent.vue
<template>
    <div>
        <h2>{{title}}</h2>
        <div>{{content}}</div>
    </div>
</template>

<script>
export default {
    props: {
        title: String,
        content: String
    }
};
</script>
App.vue
<template>
    <div>
        <SampleComponent title="Title!1" content="Content!1" />
        <SampleComponent title="Title!2" content="Content!2" />
        <SampleComponent title="Title!3" content="Content!3" />
    </div>
</template>

<script>
import SampleComponent from "./components/SampleComponent.vue";

export default {
    data: () => ({
        count: 0
    }),
    components: {
        SampleComponent
    }
};
</script>

Reactの場合

SampleComponent.js
import React from 'react';

const SampleComponent = (props) => {
    return (
        <div>
            <h2>{props.title}</h2>
            <div>{props.content}</div>
        </div>
    );
}

export default SampleComponent;
App.js
import React from 'react';
import SampleComponent from './components/SampleComponent';

const App = () => {
    return (
        <div>
            <SampleComponent title="Title!1" content="Content!1" />
            <SampleComponent title="Title!2" content="Content!2" />
            <SampleComponent title="Title!3" content="Content!3" />
        </div>
    );
}

export default App;

API通信(Axips)

こちらはVueは省略します。

useEffectはコンポーネントのレンダリング後に走らせたい処理をコールバックで渡します。その中でAxiosを使ってpokeAPIからデータを取得し、そのレスポンスをstateのセッター経由でviewに反映させます。

以下の例ではpokeAPIからポケモンの図鑑No1〜10のデータを取って、一覧表示させています。

実行結果

Screenshot from 2019-12-19 23-59-54.png

Reactコード

App.js
import React from 'react';
import Axios from "axios";

const App = () => {
    const [pokemons, setPokemons] = React.useState(null);
    const [isApiConFailed, setIsApiConFailed] = React.useState(false);

    React.useEffect(() => {
        Axios.get("https://pokeapi.co/api/v2/pokemon?offset=0&limit=10").then(response => {
            setIsApiConFailed(false);
            setPokemons(response.data.results);
        }).catch(error => {
            setIsApiConFailed(true);
        });
    }, []);

    const ApiResult = (
        pokemons !== null && (
            pokemons.map((pokemon, index) => (
                <div key={index}>
                    <h2>{pokemon.name}</h2>
                    <div>
                        <a href={pokemon.url}>このポケモンの詳細</a>
                    </div>
                </div>
            ))
        )
    );

    return (
        <div>
            {ApiResult}
            {isApiConFailed && <div>API通信エラー</div>}
        </div>
    );
}

export default App;

学習方法

1.ハローワールド
2.UdemyのReactコースや公式ドキュメントを参考に色んな機能を試す
3.Vueで作りかけだったものをReactで書き直す

React公式ドキュメント:
https://ja.reactjs.org/docs/getting-started.html

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

C#エンジニアに贈るReact入門

この記事は React #2 Advent Calendar 2019 19日目の記事です。

はじめに

本職は、C#でWindows Formを使ったレガシーなアプリケーションを作っています。

ただ、レガシーなアプリを作っていると、Webフロントエンドは、進化が早くて、活発で、
なんかキラキラしている感じがして、うらやましくなってきます。

Reactは、趣味だけなのでまだまだですが、そんなC#エンジニア(以降、C#er)から見たReact入門を解説したいと思います。

同僚がReactを勉強したいと言い出したので、そのための資料でもあります。

React の Advent Calendar ということで他の方がReactのTipsを公開している中、
一人だけ空気を読まなくて申し訳ないです・・・。

なお、Webフロントエンド界隈は進化や世代交代が早いので、
この記事の賞味期限もせいぜい半年~1年くらいだと思っています。

要約

  • C#エンジニアがReactを始めるときは、Typescript、MobX、Material-UI、Class Componentを使うといいよ。
  • 解説をつけながらハンズオン的な感じでブログっぽいページを作るよ

想定読者

  • WindowsでC#を使ってWindows Formなアプリを作ってきた方
  • および、Webアプリは、CGIとかAjaxあたりで止まっている方

React基礎知識

そもそもReactって?

Webアプリのフロントエンドを構築するためのJavascriptライブラリです。
最近の流行として、htmlのページ遷移無しで、1ページ内で完結させようという流行(SPA、Single Page Application)があり、ReactはSPAを実現するためのフレームワークの1つです。
主な役割はhtmlのレンダリングであり、C#でいうと、WPFにおけるXAMLや、WindowsFormsの*.Designer.csに相当します。

Reactって何がうれしいの?

レガシーなWebサイトの場合、htmlでコンテンツを管理して、 見た目はCSS、何か動かすならJavascriptという感じだったと思います。

ただ、htmlで使えるのは決まったタグだけで、 部品化ができず、凝ったことをしようとすると作りこみが必要なわりに、それを他のページに展開するにはコピペが必要になってしまいます。

Reactを使うと、独自のHTMLタグを増やすことができるようになります。
さらに、単なるhtmlの部品ではなく、Javascriptもセットになっているので、
部品化によってリッチなUIを簡単に作ることができるようになります。

C#でいうなら、既定のButtonやLabelしか使えない状況から、UserControlが使えるようになったものです。

公開されている部品を組み合わせてもいいですし、もちろん自分で部品を作ることもできます。

例えば、次のようなブログのトップページをイメージしてもらうと、

Sample-Blog-Top.jpg

1つの記事のサマリーがAbstractContentという部品になっていれば、 それを並べればTopページが作れます。

<div className="TopPage">
    <AbstractContent title={title1} .... />
    <AbstractContent title={title2} .... />
    <AbstractContent title={title3} .... />
</div>

さらにAbstractContentも何かの部品を組み合わせているので 次のようなツリー構造になります。

Blog-Component-Tree.jpg

部品の例としては、material-uiのサイトを眺めるとこの部品化がイメージしやすいと思います。

Reactで部品はどうやって作るのか?

Reactの部品はこんな感じで作ります。

Reactコンポーネントの例
import React from "react";

interface BlogPartsProps {
  id: number;
  title: string;
  date: string;
  abstract: string;
}

class BlogParts extends React.Component<BlogPartsProps > {
  public render(): JSX.Element {
    return (
      <div>
        <a href={`/contents/${this.props.id}`}><p>{this.props.title} {this.props.date}</p></a>
        <p>{this.props.abstract}</p>
      </div>
    );
  }
}

export default BlogParts;

細かい説明は、他サイトを参考にしてもらったらよいと思いますが、
ポイントは次の2つです。

  • render関数では、JavascriptとHtmlが混ざった書き方をする。C#でいうと、RazorやT4テンプレートみたいなもの。
  • BlogPartsProps interfaceに定義されているフィールドが、外から渡されるパラメータ

BlogPartsPropsうんぬんは、C#のWindows Formで例えると、こんな感じで、外からセットされるプロパティに相当します。

C#でのPropsの例
class BlogParts : UserControl
{
  public int id {get;set;}
  public string title {get;set;}
  public string date {get;set;}
  public string abstractContent {get;set;}

  private void BlogParts_Load(object sender, EventArgs e)
  {
      _titleLabel.Text = this.Title;
      ・・・
  }
}

なお、Reactは、classで部品を作るパターン(Class Component)と、関数で部品を作るパターン(Functional Component)がありますが、C#erとしては、前者のほうがイメージしやすいです。
ただし、今の流行や推奨は後者のFunctional Componentです。React Hooksとかもこちらのようです。

今回は、C#er向けということでClass Componentでいきます。

Typescript

Typescriptは、Javascriptに型を導入した言語です。
ビルド(トランスクリプトというらしい)するとjavascriptになります。
ビルド結果のjavascriptは人が読むことは想定されていないようなので
ビルドするとバイナリになるC#に近い使用感です。
生のJavascriptは型がなく、なんとなくで実装できてしまいますが、
C#をやっている身からすると型が無いのが恐ろしいです。

ということで、Typescript、おすすめです。

言語的にはC#に近い言語っぽいけど、Javascriptの制約を引きずっているのと、独自進化しているようで、
知らない仕様がけっこうあります。

簡単なクラスを例にするとこんな感じです。

Typescriptの例
export class AppStore
{
    _id:number|undefined;  // 型はメンバーの後ろ、また、型に"|"を使ってORを表現できる。

    // メソッドはラムダ式を代入したメンバー。C#のメソッドっぽくも書けるけどこっちのほうがいいみたい
    public changeId = (id:number):void => {  
        this._id = id;
    }
    public getId = ():number => {
        if(this._id !== undefined)  //undefinedでないことをチェックしないとビルドが通らない
        {
            return this._id;
        }
        return 0;
    }
}

Windowsアプリでも問題になるけど内部状態はどう管理したらいいの?

React界隈でも問題になったようで、Flaxというアーキテクチャが生み出されています。

flux.jpg

参考: https://facebook.github.io/flux/

簡単に解説すると、(1)内部状態(Store)は1か所に集約する、
(2)GoFのCommandパターンで状態を更新する(ActionとDispatcher)、
(3)Viewは内部状態をただ表現するだけ、のようです。
この発想自体は、Windowsアプリケーションでも参考になりそうですね。

ただ、理念は良さそうですが、Flax実装の一つで一番人気のReduxは肌に合わなかったです。
単にReduxの実装がよくないだけかもしれませんし、
Commandパターンにありがちな、無駄に実装量が増えるあたりかもしれません。

代替策を調べた結果、もう少し簡易的な状態管理ライブラリである、MobXが良さそうに思えました。
MobXは、上の(1)~(3)の中では、(2)はあきらめて、
DIとGoFのObserverパターンを追加した感じです。
WPFにMVVMパターンがありますが、それに似ています。

すなわち、いろいろなViewModelは1か所で管理されて、Viewの定義に従ってDIされる、
ViewModelで状態が変わるとView側(React)に通知が来て、再描画される、という感じです。

MobX_Abstract.jpg

C#でMobXに似たような実装を書くとこんな感じ。

C#でのMobXっぽい実装イメージ
public class App: UserControl
{
    ViewModel _viewModel;

    [Inject("viewModel")]  //この属性を目印にDIされる
    public void SetViewModel(ViewModel viewModel)
    {
        _viewModel = viewModel;
        _viewModel.PropertiesUpdated += _viewModel_PropertiesUpdated;
    }

    _textBox1_TextChanged()
    {
        _viewModel.Text = _textBox1.Text;  //コントロールの変更はViewModelに伝える
    }

    _viewModel_PropertiesUpdated()  // ViewModelの変更から画面を更新する
    {
        // 描画更新
    }
}

public class ViewModel
{
    private string _text;
    public string Text {
        get { return _text; }
        set { _text = value; PropertiesUpdated?.Invoke();}  //プロパティの変更を通知する
    }
    public event Action PropertiesUpdated;
}

参考: https://mobx.js.org/README.html

Reactをとにかく使ってみたい

さて、基礎知識はここまでとして、実際に手を動かしてみましょう。
思ったより長くなったので、興味ない方は、以降は読み飛ばして、まとめに行ってもらってもいいです。

事前に必要なもの

環境は、C#er前提なのでWindowsです(ほかの環境でも動きそうですが確認していません)。
また、次のアプリをインストールしておく必要があります。

なお、React本体やTypescriptコンパイラは開発用フォルダにダウンロードされますので 上記以外ではOSを汚しません。(・・・はず)

開発環境

まずは開発環境を作ります。 次のコマンドで、create-react-appというReact開発環境のテンプレートを生成するツールをダウンロードします。なお、npmはC#でいうところのNugetです。

commandline
cd (適当な空フォルダー)
npm install create-react-app

ダウンロードしたモジュールはnode_modulesフォルダに入っており、実行モジュールはnode_modules/.binにあります。
それを使って、開発環境を生成します。
第一引数はフォルダ名(およびプロジェクト名)を、第二引数の--typescriptはJavascriptではなくTypescriptを使うことを指定しています。

commandline
node_modules\.bin\create-react-app test --typescript

出来上がった開発環境のフォルダに移動して、実行してみましょう。

commandline
cd test
npm start

初期の画面が表示されます。 npm start は、Visual Studioのデバッグの開始のようなもので、
自動的にhttpサーバーが起動し、ソースがビルドされて、Webページが表示されます。

React-init-screen.jpg

この状態で、ソースコードを一部変更してみます。
Visual Studio Code等のテキストエディタでsrc/App.tsxを編集します。

src/App.tsx
- Edit <code>src/App.tsx</code> and save to reload.
+ Hello World

ソースコードを変更すると自動でビルドされ、Webページも更新されます。

React-init-screen-Helloworld.jpg

終了は、コマンドプロンプトでCtrl+Cです。

開発環境のファイル構成はこんな感じです。
よく使うファイルは説明を付けました。

├ build         ・・・ビルド結果が入る。C#でいうとbin/Release
├ node_modules  ・・・ダウンロードしたパッケージがあります。C#でいうとpackages
├ public        ・・・この中身がビルド時にコピーされる。index.htmlとか入っている
├ src           ・・・ソースコードフォルダ
│  ├ App.css
│  ├ App.test.tsx ・・・App.tsxのユニットテスト。
│  ├ App.tsx      ・・・Web画面。ここをメインで編集します。C#でいうとForm1.Designer.cs
│  ├ index.css
│  ├ index.tsx    ・・・エントリーポイントです。C#でいうとProgram.csのmain関数
│  ├ logo.svg
│  ├ react-app-env.d.ts
│  ├ serviceWorker.ts
│  └ setupTests.ts
├ .gitignore
├ package.json      ・・・このプロジェクトの情報が記載されている。C#でいうと*.csproj
├ package-lock.json ・・・依存パッケージの情報。package.jsonにもあるけど、こっちがマスター?
├ README.md
└ tsconfig.json     ・・・Typescriptの設定。

開発環境を作りこむ

このままでも開発可能ですが、コードチェックの設定を行って、楽をしましょう。
C#でいうと、Visual Studioのコード分析か、Resharperですね。

必要なパッケージをダウンロードします。

commandline
npm install --save-dev @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-prettier eslint-plugin-prettier prettier

コードチェックのルール用ファイルを作成します。

.eslintrc.json
{
    "extends": [
      "react-app",
      "eslint:recommended",
      "plugin:@typescript-eslint/recommended",
      "plugin:@typescript-eslint/eslint-recommended",
      "plugin:prettier/recommended",
      "prettier/@typescript-eslint"
    ],
    "plugins": [
      "@typescript-eslint"
    ],
    "parser": "@typescript-eslint/parser",
    "env": { "browser": true, "node": true, "es6": true },
    "parserOptions": {
      "sourceType": "module"
    },
    "rules": {

    }
  }

次のコマンドでソースコードのコードチェックが行われます。

commandline
node_modules/.bin/eslint src --ext .js,.jsx,.ts,.tsx
コードチェック結果
(WorkDir)\src\App.test.tsx
  1:19  error  Replace `'react'` with `"react"`                                        prettier/prettier
  2:24  error  Replace `'@testing-library/react'` with `"@testing-library/react"`      prettier/prettier
  3:17  error  Replace `'./App'` with `"./App"`                                        prettier/prettier

・・・

✖ 39 problems (33 errors, 6 warnings)
  33 errors and 0 warnings potentially fixable with the `--fix` option.

デフォルト状態では、大量にエラーが表示されます。

次に、Visual Studio Codeでの警告表示と保存時の自動Fixを設定します。
Visual Studio Codeの設定ファイルに以下を追加します。ファイルやフォルダがなければ新規作成してください。

.vscode\settings.json
{
    "eslint.autoFixOnSave": true,
    "eslint.validate": [
        "javascript",
        "javascriptreact",
        {"language": "typescript", "autoFix": true },
        {"language": "typescriptreact", "autoFix": true }
      ]
}

これで、Visual Studio CodeでJavascriptやTypescriptのファイルを開いた場合に以下のように警告が表示され、
上書き保存時に自動修正できる箇所は修正されます。

VSCode-Warning.jpg

eslintの出力に記載があるように--fixをつけると自動Fixできますが、ファイル数も少ないので、
Visual Studio Codeで1つ1つファイルを開いては上書き保存していきます。

もう一度eslintを実行すると、自動解決できない警告だけになっています。
ここでは、気にしない方向で無視します。
なお、@typescript-eslint/explicit-function-return-typeを無視するようにeslintrc.jsonに記載するか、serviceWorkerの関数の戻り値をちゃんと宣言すれば、警告を消せます。

commandline
node_modules/.bin/eslint src --ext .js,.jsx,.ts,.tsx
コードチェック結果
(WorkDir)\src\serviceWorker.ts
   28:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   62:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   66:36  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   71:42  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  106:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  136:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type

✖ 6 problems (0 errors, 6 warnings)

毎回、上記のeslintコマンドを入れるのは面倒なので、mpn scriptにします。 (なおnode_modules/.bin のパスは、npmコマンドで実行すると解決されるので不要です)

package.json
-   "eject": "react-scripts eject"
+   "eject": "react-scripts eject", 
+   "lint": "eslint src --ext .js,.jsx,.ts,.tsx"

これで、npm run lintで実行できるようになります。

commandline
npm run lint
コードチェック結果
(WorkDir)\src\serviceWorker.ts
   28:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   62:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   66:36  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
   71:42  warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  106:1   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type
  136:8   warning  Missing return type on function  @typescript-eslint/explicit-function-return-type

✖ 6 problems (0 errors, 6 warnings)

参考

Class Componentにする

App.tsxのデフォルトは、Functional Componentです。
上で話した通り、C#er的にはClass Componentのほうがわかりやすいので、Class Componentに変更します。

といっても、わりと機械的な作業で、 React.Componentを継承したclassを作って、
デフォルトで実装されていたApp関数をrender関数にします。

src/App.tsx(変更前)
const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello World</p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
};
src/App.tsx(変更後)
class App extends React.Component {
  public render = (): JSX.Element => {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>Hello World</p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  };
}

ポイントは以下です。

  • React.Componentを継承したclassを作ります。なお、Typescriptの継承はextendsです。
  • reander関数の戻り値はhtmlっぽい何かで、型的にはJSX.Elementになります。
    • classclassName にする(javascriptの構文と名前が被るので)
    • Htmlの属性は文字列以外は{}で囲ってJavascriptまたはTypescriptで記載する
    • styleは {{width:"50%"}} のように{}で囲われたJsonで書く

npm startしてみると、当然ですが、見た目は変わりません。

React-init-screen-Helloworld.jpg

参考: https://qiita.com/daikoncl/items/a3806d8a8bf35f086487

Material-UIを導入する

Reactのデフォルトでは、標準のhtmlタグしか使えません。 デザインセンス無しでそこそこイケてるWeb画面を作るために、既存の部品を使います。
ここでは、上で紹介した、Material-UIです。

まずパッケージをインストールします。

commandline
npm install @material-ui/core @material-ui/icons --save

そして、material-uiを使ったUIにApp.tsxをマルっと書き換えます。
Cardを使って、ブログのサマリーページ的な感じにしました。
Cardの使い方は、Material-UIの公式を参考に。
https://material-ui.com/components/cards/

src/App.tsx
import React from "react";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import "./App.css";

class App extends React.Component {
  render(): JSX.Element {
    return (
      <div className="App">
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader
            action={
              <IconButton aria-label="settings">
                <MoreVertIcon />
              </IconButton>
            }
            title="タイトル"
            subheader="2019-12-17"
          />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              アブストラクトです。
            </Typography>
          </CardContent>
        </Card>
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader
            action={
              <IconButton aria-label="settings">
                <MoreVertIcon />
              </IconButton>
            }
            title="タイトルその2"
            subheader="2019-12-17"
          />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              アブストラクトその2です。
            </Typography>
          </CardContent>
        </Card>
      </div>
    );
  }
}

export default App;

npm startしてみると次のようになります。
いろいろスタイルは手抜きですが、影とかアイコンとかそれっぽくなりました。

React-blog-top.jpg

参考: https://material-ui.com/

データをサーバーから取得

現状、コンテンツはApp.tsxに埋め込まれています。
普通のサイトを考えるとJsonをサーバーから取得すると思います。

ざっくりJsonを想定するとこんな感じでしょうか。

[
  {
    "id": 1
    "title": "タイトル",
    "date": "2019-12-17",
    "abstract": "アブストラクトです。"
  },
  {
    "id": 2
    "title": "タイトルその2",
    "date": "2019-12-17",
    "abstract": "アブストラクトその2です。"
  }
]

Webページの組み込みは、いったん後回しとして、 サーバーからJsonを取得して、このデータをWebページに渡すViewModelを作り、 単体テストで動作確認します。

まずは、サーバーがないのでテスト時にHTTPリクエストを代替するパッケージfetch-mockを入れます。

commandline
npm install fetch-mock @types/fetch-mock node-fetch --save-dev

そして、ViewModelとユニットテスト用に2つのファイルを追加します。

src/AppStore.ts (ViewModelです)
export interface BlogSummary {
  id: number;
  title: string;
  date: string;
  abstract: string;
}

export class AppStore {
  data: BlogSummary[] = [];
  public fetchData = async (): Promise<void> => {
    const json = (await (
      await fetch("/api/summaries/")
    ).json()) as BlogSummary[];
    this.data = json;
  };
}
src/AppStore.test.ts (ユニットテストです)
import { AppStore } from "./AppStore";
import fetchMock from "fetch-mock";

const dummyData = [
  {
    id: 1,
    title: "タイトル",
    date: "2019-12-17",
    abstract: "アブストラクトです。"
  },
  {
    id: 2,
    title: "タイトルその2",
    date: "2019-12-17",
    abstract: "アブストラクトその2です。"
  }
];
fetchMock.get("/api/summaries/", dummyData);

test("fetchData test", () => {
  const target = new AppStore();
  target.fetchData().then(() => {
    expect(target.data).toEqual(dummyData);
  });
});

ついでに、上の変更ですでに壊れているので src/App.test.tsx を削除します。
(個人的にViewのテストは難しいわりにリターンが少ないので、あまりしません。
Typescriptで型はチェックできていますし。)

ユニットテストを実行してみましょう。

commandline
npm run test

成功しました。良さそうです。

ユニットテスト結果
 PASS  src/AppStore.test.ts
  √ fetchData test (5ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.573s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.

参考: http://www.wheresrhys.co.uk/fetch-mock/

MobXを使ってViewModelとViewを結合する

ViewModel AppStore はルートである Index.tsxstores で管理し、App にインジェクトします。

Object-Diagram1.jpg

まずは必要なパッケージをインストールします。

commandline
npm install mobx mobx-react --save

そして、MobXはデコレーターを使用するのでTypescriptのオプションを変更します。

tsconfig.json
-    "jsx": "react"
+    "jsx": "react",
+    "experimentalDecorators": true
  },

index.tsxでストア(stores)を作り、 状態を管理する<Provider></Provider>Appを囲い、
Providerにはstoresを渡します。
これにより、Appの要求に従って、Providerから必要なViewModelを渡されます。

src/index.tsx
- ReactDOM.render(<App />, document.getElementById("root"));
+ import { AppStore } from "./AppStore";
+ import { Provider } from "mobx-react";
+ 
+ const stores = {
+   appStore: new AppStore()
+ };
+ 
+ ReactDOM.render(
+   <Provider {...stores}>
+     <App />
+   </Provider>,
+   document.getElementById("root")
+ );

ストアの appStore を受け取るように App で宣言します。

src/App.tsx(変更前)
class App extends React.Component {
src/App.tsx(変更後)
import { AppStore } from "./AppStore";
import { inject, observer } from "mobx-react";

interface AppProps {
  appStore?: AppStore;
}

@inject("appStore")
@observer
class App extends React.Component<AppProps> {

ポイントは以下です。

  • React部品で外からパラメータを受け取るにはPropsの仕組みを使います。
    • React.Componentのジェネリック型に、受け取るパラメータを宣言したインターフェイスAppPropsを指定します。
    • 今回はstoresで管理されているAppStoreを受け取るので、AppStore型のappStoreを宣言します。
  • @injectの引数およびAppPropsのプロパティには、受け取りたいstores内のプロパティ名appStoreを指定します。

これで、index.tsxstores内のappStoreAppに渡されます。

なお、当然ですが、見た目は変わりません。

React-blog-top.jpg

ViewModelを使ってWebページを更新する

上までで、AppにViewModelであるAppStoreが渡されたので、AppStoreの状態を使ってWebページを更新するようにします。 App.tsxをマルっと書き換えます。

src/App.tsx
@inject("appStore")
@observer
class App extends React.Component<AppProps> {
  // コンポーネントが構築される直前に呼ばれる
  public componentDidMount = (): void => {
    if (this.props.appStore === undefined) {
      return;
    }
    this.props.appStore.fetchData();
  }

  render(): JSX.Element {
    // appStoreはundefindedである場合があるので、その時は何も表示しない
    if (this.props.appStore === undefined) {
      return <div />;
    }

    // this.props.appStore.dataに従ってWebページを構築する
    return (
      <div className="App">
        {this.props.appStore.data.map(data => {
          return (
            <Card key={data.id} style={{ width: 300, margin: 2 }}>
              <CardHeader
                action={
                  <IconButton aria-label="settings">
                    <MoreVertIcon />
                  </IconButton>
                }
                title={data.title}
                subheader={data.date}
              />
              <CardContent>
                <Typography color="textSecondary" gutterBottom>
                  {data.abstract}
                </Typography>
              </CardContent>
            </Card>
          );
        })}
      </div>
    );
  }
}

ポイント

  • componentDidMount() はこのコンポーネントが構築される前に呼ばれます。
    • C#でいうところの、Form.Loadイベントです。
    • ここでは、サーバーからデータを取得するため、 appStore.fetchData() を呼び出します。
  • render() では、 this.props.appStore.data に従ってWebページを構築します。
    • {}で囲うとTypescriptを記述でき、returnでJSX.Elementを返します。

MobXは、プロパティが変更されたことを検知して、Viewを再描画させる機能を持っています。
これを使って、データが取得されたときにViewを更新します。
そのために AppStore のメソッドとプロパティに属性を付けます。

src/AppStore.ts
+ import { action, observable } from "mobx";


 export class AppStore {
+  @observable
   data: BlogSummary[] = [];
+  @action
   public fetchData = async (): Promise<void> => {
 }

これでサーバーデータに応じた画面を作ることができましたが、
このままでは、サーバーは存在しないのでエラーになります。
ユニットテスト用のfetch-mockを流用してダミーデータを使うようにします。

src/index.tsx
+ import fetchMock from "fetch-mock";
+ 
+ const dummyData = [
+   {
+     id: 1,
+     title: "[サーバー]タイトル",
+     date: "2019-12-17",
+     abstract: "アブストラクトです。"
+   },
+   {
+     id: 2,
+     title: "[サーバー]タイトルその2",
+     date: "2019-12-17",
+     abstract: "アブストラクトその2です。"
+   }
+ ];
+ fetchMock.get("/api/summaries/", dummyData);

  const stores = {
    appStore: new AppStore()
  };

これで、 npm start すると以下のようになります。

React-blog-top-json.jpg

dummyDataをいろいろ変えてみるとWebページが変わると思います。

部品を作る

Reactの部品化の練習のためにTOPページのサマリー要素部分を部品化してみます。

Object-Diagram2.jpg

上と同じようにReactコンポーネントのclassを作って、
renderでは、App.tsxで部品化したい部分を返すようにします。
必要なパラメータはPropsで上から渡してもらいます。
なお、Storesで状態管理するのはページごとで、部品ではViewModelは使わないほうが良いようです。

src/SummaryItem.tsx(新規作成)
import React from "react";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import { Link } from "react-router-dom";

interface SummaryItemProps {
  title: string;
  date: string;
  abstract: string;
  url: string;
}

class SummaryItem extends React.Component<SummaryItemProps> {
  render(): JSX.Element {
    return (
      <Card style={{ width: 300, margin: 2 }}>
        <CardHeader
          action={
            <IconButton aria-label="settings">
              <MoreVertIcon />
            </IconButton>
          }
          title={<Link to={this.props.url}>{this.props.title}</Link>}
          subheader={this.props.date}
        />
        <CardContent>
          <Typography color="textSecondary" gutterBottom>
            {this.props.abstract}
          </Typography>
        </CardContent>
      </Card>
    );
  }
}

export default SummaryItem;

App.tsxのほうは、上で作った部品を使うように書き換えます。
なお、部品の参照は、importです。
また、不要になったimportも消しておきましょう。

src/App.tsx
- import Card from "@material-ui/core/Card";
- import CardHeader from "@material-ui/core/CardHeader";
- import CardContent from "@material-ui/core/CardContent";
- import IconButton from "@material-ui/core/IconButton";
- import MoreVertIcon from "@material-ui/icons/MoreVert";
- import Typography from "@material-ui/core/Typography";
+ import SummaryItem from "./SummaryItem";


-            <Card key={data.id} style={{ width: 300, margin: 2 }}>
-              <CardHeader
-                action={
-                  <IconButton aria-label="settings">
-                    <MoreVertIcon />
-                  </IconButton>
-                }
-                title={data.title}
-                subheader={data.date}
-              />
-              <CardContent>
-                <Typography color="textSecondary" gutterBottom>
-                  {data.abstract}
-                </Typography>
-              </CardContent>
-            </Card>
+            <SummaryItem
+              key={data.id}
+              title={data.title}
+              date={data.date}
+              abstract={data.abstract}
+            />

相変わらず見た目は変わりませんが、
サマリー部分の部品化ができました。

React-blog-top-json.jpg

なお、この部品は再利用しないので、部品化はあまり意味はありません。

ページ遷移を作る

SPAといえども、普通のHtmlのようにページ遷移がしたいことが多いです。
概要←→詳細 で行き来することもありますし、1ページいろいろ押し込むと使いにくいですし。

ここでは、React-Routerを使ってページ遷移を作ります。

まずページ遷移があるということはURLを設計しないといけません。
簡単ですが、以下のようにします。

  • http://~/         → トップページ(サマリーのページ)
  • http://~/contents/(Id)  → 詳細ページ(Idは数字)

では作ってみましょう。

まず、必要なパッケージを取得します。

commandline
npm install react-router-dom @types/react-router-dom  --save

次にTOPページから詳細ページへのリンクを作ります。
React-Routerでのリンクは<Link to="url">ラベル</Link>を使いますので、
タイトル部分をリンクに置き換えます。
URLはPropsに追加して、親ページから渡してもらうことにします。

src/SummaryItem.tsx
  import Typography from "@material-ui/core/Typography";
+ import { Link } from "react-router-dom";

  interface SummaryItemProps {
    title: string;
    date: string;
    abstract: string;
+   url: string;
  }

             </IconButton>
           }
-          title={this.props.title}
+          title={<Link to={this.props.url}>{this.props.title}</Link>}

親ページでは、URLを渡すように設定します。

src/App.tsx
              <SummaryItem
                key={data.id}
+               url={`./contents/${data.id}`}
                title={data.title}
                date={data.date}
                abstract={data.abstract}

これでトップページからのリンクはできました。
では、詳細ページを作っていきます。

作り方は、App.tsxと同じでページ(ContentPage.tsx)とViewModel(ContentPageStore.ts)を作って
MobXで連携させます。

Object-Diagram3.jpg

src/ContentPage.tsx (新規作成)
import React from "react";
import { RouteComponentProps } from "react-router-dom";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import IconButton from "@material-ui/core/IconButton";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import Typography from "@material-ui/core/Typography";
import { ContentPageStore } from "./ContentPageStore";
import { inject, observer } from "mobx-react";

interface ContentPageProps extends RouteComponentProps<{ id: string }> {
  contentPageStore?: ContentPageStore;
}

@inject("contentPageStore")
@observer
class ContentPage extends React.Component<ContentPageProps> {
  public componentDidMount = (): void => {
    if (this.props.contentPageStore === undefined) {
      return;
    }
    const id = parseInt(this.props.match.params.id, 10);
    this.props.contentPageStore.fetchData(id);
  };

  render(): JSX.Element {
    if (this.props.contentPageStore === undefined) {
      return <div />;
    }
    const store = this.props.contentPageStore;
    return (
      <div className="App">
        <Card style={{ width: 300, margin: 2 }}>
          <CardHeader title={store.data.title} subheader={store.data.date} />
          <CardContent>
            <Typography color="textSecondary" gutterBottom>
              {store.data.content}
            </Typography>
          </CardContent>
        </Card>
      </div>
    );
  }
}

export default ContentPage;
src/ContentPageStore.ts (新規作成)
import { action, observable } from "mobx";

interface BlogContent {
  id: number;
  title: string;
  date: string;
  content: string;
}

export class ContentPageStore {
  @observable
  data: BlogContent = {
    id: 0,
    title: "",
    date: "",
    content: ""
  };

  @action
  public fetchData = async (id: number): Promise<void> => {
    const json = (await (
      await fetch(`/api/contents/${id}`)
    ).json()) as BlogContent;
    this.data = json;
  };
}

React-Routerの本番はここからです。
今までindex.tsx<App/>と直接Reactコンポーネントを指定していたところを、
<BrowserRouter>で囲って、<Route/>を並べていきます。
<Route>component=にReactコンポーネントを、path=にURLを指定します。

また、ViewModelもStoresに追加します。

src/index.tsx
  import fetchMock from "fetch-mock";
+ import ContentPage from "./ContentPage";
+ import { ContentPageStore } from "./ContentPageStore";
+ import { BrowserRouter, Route } from "react-router-dom";


  const stores = {
-   appStore: new AppStore()
+   appStore: new AppStore(),
+   contentPageStore: new ContentPageStore()
  };
  ReactDOM.render(
    <Provider {...stores}>
-     <App />
+     <BrowserRouter basename={"/"}>
+       <Route exact={true} path="/" component={App} />
+       <Route exact={true} path="/contents/:id" component={ContentPage} />
+     </BrowserRouter>
    </Provider>,
    document.getElementById("root")
  );

これで、/の場合はAppのページが、/contents/(id)の場合は、ContentPageのページが表示されるようになります。
URLのルートURLは、<BrowserRouter basename=に指定します。

昔ながらのCGIをやっていた方は、URLが違うなら別のhtmlが返ってくるのでは?という気がしますが、
SPAの場合は、バックエンド側で同じindex.htmlを返すように設定するようです。

一応これで完成なのですが、相変わらずバックエンドがないので
ダミーデータを返すようにします。

src/index.tsx
-     abstract: "アブストラクトです。"
+     abstract: "アブストラクトです。",
+     content: "これは本文です。"
    },
    {
      id: 2,
      title: "[サーバー]タイトルその2",
      date: "2019-12-17",
-     abstract: "アブストラクトその2です。"
+     abstract: "アブストラクトその2です。",
+     content: "これは本文です。"
    }
  ];
  fetchMock.get("/api/summaries/", dummyData);
+ fetchMock.get("/api/contents/1", dummyData[0]);
+ fetchMock.get("/api/contents/2", dummyData[1]);

以上で、簡単ですがブログっぽいページができました。
TOPページからリンクをたどると、詳細ページに飛びます。
URLも変化していますので、F5を押しても元ページに戻ったりしません。

React-blog-top-withLink.jpg

React-blog-content.jpg

ビルドしてデプロイ可能にする

ある程度アプリができたら、ビルドしてデプロイできるようにします。
次のコマンドを実行すると、buildフォルダに成果物が出来上がります。

commandline
npm run build

その他細かいTips

多言語対応は?

Javascript用にいくつかライブラリがあるようです。

基本的には、言語ごとにJsonを用意して、キーとなる文字列を指定すると、
実行時には言語に応じたJsonから文字列が取れるようです。

リソースの定義
{
  "buttons":{
      "new":"新規投稿"
      "edit":"編集"
  }
}
多言語リソースを使う例
   <Button label={i18next.t('buttons.new')} />

C#のResources.resxと比較すると、型がないので、インテリセンスが使えない分、ちょっと面倒です。

以前やったときに、Typescript+Jsonスキーマを使うといい感じに改善できそうだったので、
また記事を書くかもしれません。

サーバーサイドはどうする?

Node.jsもいいですが、C#erなら、過去のC#の資産を使えるASP.NET Coreを使いたいです。

Visual Studioで[プロジェクトの新規作成]-[ASP.NET Core Webアプリケーション]→[React.js]で
テンプレートを作って、ClientApp以下を上と同じ手順で作ったReact開発環境で置き換えれば、
いいようです。

普通にF5実行で、Reactのフロントエンド+C#のバックエンドで起動しました。
なお、初回はビルドが走るのか重いようです。

問題は、ASP.NET Coreを動かすサーバーを用意できるかどうか。

ReactとWindows Formで開発速度はどれくらい違う?

htmlが不慣れとかいろいろ要素がありますが、
私の場合、Windows Formで作るほうが3倍くらい速いです。

C# ←→ Typescriptの変換も面倒ですし、
画面レイアウトの作りこみも大変です。
というか、C#を振り返ると、Visual StudioのWindows Formのデザイナーはよくできているなぁと思います。
(壊れやすいのが難点ですが)

まとめ

割とがっつりReactの入門記事になってしまいましたが、
普段C#を使っている方の視点で、解説したつもりです。
Reactを使ってSPAを始めるための一通りを説明したつもりなので、
あとは応用を学んでいけば、そこそこのプロダクトが作れるのではないでしょうか。

なお、上で作ったコードは、一応GitHubに公開しておきます。
(この記事の賞味期限が有効なあたりまでは残しておきます)
https://github.com/banban525/react-example-2019

自分で打ち込むのが面倒な方はそちらを見てもらってもよいと思います。
個人的には写経はおすすめですが。

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

React-Bootstrapで色々使ってみる -Navbar-

React-Bootstrapを使えるようにするでboot-strapが使えるようになったので、色々触ってみる。今回はNavbar。
ほぼ本家のコピペなので、詳しくは本家参照。

Navbar

ヘッダーにあたる部分。多分全アプリで必要だと思う。

とりあえずコピペしてみる。

App.js
import React, { Component } from 'react';
import { Navbar, Nav } from 'react-bootstrap';
import LOGO from './logo.svg';

class App extends Component {
    render() {
        return (
            <Navbar bg="dark" variant="dark">
                <img
                    src={LOGO}
                    width="30"
                    height="30"
                    alt="React Bootstrap logo"
                />
                <Nav className="mr-auto">
                    <Nav.Link href="#home">Home</Nav.Link>
                    <Nav.Link href="#features">Features</Nav.Link>
                    <Nav.Link href="#pricing">Pricing</Nav.Link>
                </Nav>
            </Navbar >
        );
    }
}
export default App;

↓実行結果
スクリーンショット 2019-12-18 23.39.36.png

これだとスマホとか画面が小さくなったときに選択肢が多いと、画面崩れが起きてしまう。悲しい。
スクリーンショット 2019-12-18 23.43.39.png



スマホとかでレスポンシブに動かしたいんや!!というときは、
下記のように書くといい感じにレスポンシブにしてくれるっぽい。

app.js
import React, { Component } from 'react';
import { Navbar, Nav } from 'react-bootstrap';
import LOGO from './logo.svg';

class App extends Component {
    render() {
        return (
            <Navbar collapseOnSelect expand="lg" bg="dark" variant="dark">
                <Navbar.Brand href="#home">
                    <img
                        src={LOGO}
                        width="30"
                        height="30"
                        alt="React Bootstrap logo"
                    />
                    SampleApp
                </Navbar.Brand>
                <Navbar.Toggle aria-controls="responsive-navbar-nav" />
                <Navbar.Collapse id="responsive-navbar-nav">
                    <Nav className="mr-auto">
                        <Nav.Link href="#home">Home</Nav.Link>
                        <Nav.Link href="#features">Features</Nav.Link>
                        <Nav.Link href="#pricing">Pricing</Nav.Link>
                    </Nav>
                </Navbar.Collapse>
            </Navbar >
        );
    }
}
export default App;

スクリーンショット 2019-12-18 23.51.20.png

expand="true"にするとずっとハンバーガーメニューが表示されるようになる。
で囲った部分は、collapseされずに表示される

公式が用意していない色への変更とかはそのうち書く。

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

勉強しながらTodoアプリを更新していく:Part1

Reactを勉強 ⇒ Todoアプリに反映

最近いろいろ勉強しだして知識が散らかっていて整理したいなーと思ったので、
最小限のTodoアプリから作成し、少しずつ学習した要素を反映させていきます。
今回はPert1なので簡単なTodoアプリを作成します。

予定(は未定)

Part1 : Todoアプリ作成
 ⇒ React, State, create-react-app
Part2 : styled-component使う
 ⇒ CSS in JS
Part3 : Redux(or Flex?)使う
 ⇒ State管理
Part4 : Material UI使う (or react-bootstrap?)
 ⇒ モダンな見た目の作成
Part5 : Router 使う
 ⇒ ページ遷移(historyAPI)

Pert 1 : Todoアプリを作成

まずは最小限のTodoアプリを作ります。
学習要素的にはまずは下記の内容でしょうか。

  • Reactがどういうものか
  • componentの考え方・作ってみる
  • Stateの考え方

1. テンプレート作成

$ npx create-react-app

2. Todoコンポーネントを作成

ディレクトリとファイルを作成

$ cd src
$ mkdir components
$ cd components
# Mac
$ touch Todo.js
# Win(PowerShell)
$ New-Item -Type File  Todo.js

Todoコンポーネント作成

Todos.js
import React, { useState } from 'react';

const initTodos = [
  { id: 1, title: 'Todoアプリを作る', done: true },
  { id: 2, title: 'styled-componentを使ってみる', done: false },
  { id: 3, title: 'Reduxを使ってみる', done: false }
];

const styles = {
  done: {
    color: '#bbb',
    textDecoration: 'line-through'
  }
};

function Todos() {
  const [todos, setTodos] = useState(initTodos);
  const [nextTodo, setNextTodo] = useState("");

  const toggleDone = e => {
    const updatedTodos = todos.map(todo => {
      if (todo.id === Number(e.target.id)) todo.done = !todo.done;
      return todo;
    });
    setTodos(updatedTodos);
  };

  const changeTodo = e => {
    setNextTodo(e.target.value);
  };

  const addTodo = () => {
    const addTodo = {
      id: todos.length + 1,
      title: nextTodo,
      done: false
    };

    setTodos([...todos, addTodo]);
    setNextTodo("");
  };

  return (
    <div>
      <h1>Todos</h1>
      <p>
        <input type='text' value={nextTodo} onChange={changeTodo}/>
        <button onClick={addTodo}>Add</button>
      </p>
      {todos.map(todo => (
        <p
          key={todo.id}
          style={todo.done ? styles.done : null}
          id={todo.id}
          onClick={toggleDone}
        >
        {todo.title}
      </p>)
      )}
    </div>
  );
}

export default Todos;

App.jsの修正

App.js
import React from 'react';
import './App.css';
import Todos from './components/Todos'


function App() {
  return (
    <div className="App">
      <Todos/>
    </div>
  );
}

export default App;

Todoの追加、実施/未実施の切り替えができる簡単なTodoアプリが作成できました。

todos.gif

下切れてるんですけど...

まとめ

まとめるほどの内容でもありませんがコンポーネント作成からstyleの適用、
Stateの扱い方くらいの程よいボリュームではないかと思います。

次回はstyled-componentを使ってみたいと思います。
(全部Reactの世界に閉じるらしい...すげぇ!)

コードはここ

勉強になる素晴らしい記事

世の中には分かりやすい説明をしてくれる人がいるのでせっかく勉強するなら
そんな人たちの記事を読む+公式Docさえ読めば理解が早くて効率が良いはず...!

https://qiita.com/koba04/items/4d13caf5ab4507974bf0
https://qiita.com/tomzoh/items/7fabe7cb57dd96425867
https://qiita.com/toshi-toma/items/6ce7a3aa72b342751e0c#_reference-41d742e090778def3b03

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