- 投稿日:2019-12-19T23:44:31+09:00
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/74801923ca108b328b26GitHub Actionsの設定
1. GitHub Actions のページを開く
GitHubのリポジトリで[Actions]ページを開きます。
2. テンプレートの作成
[Node.js]のテンプレートを選択します。
.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うごきました。
無事にデプロイもされました。たったこれだけで、無事にビルドされて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-19T22:50:15+09:00
DECK.GLを使ってGoogleMapタイムラインをビジュアライズしてみよう!#4 〜Mapコンポーネントの表示〜
DECK.GLを使ってGoogleMapのタイムラインをビジュアル化してみよう!の第4弾の記事です。
前回はDECK.GLのPathLayerを使って、データの描画を行いました。
今回はこれにマップコンポーネントを組み込んで表示させてみたいと思います。前回のおさらい
前回は、App.jsx に下記のコードを書きました。
App.jsimport 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のGithubMapboxを連携させよう
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ボタンから作成することができます。こちらからログインしてください
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コマンドで起動させると・・・
地図の上にパスが表示されました!!
次回予告
次回からはいよいよGoogleMapタイムラインの表示を行なっていきます!
App.jsxの全体像
App.jsimport 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> ); } }
- 投稿日:2019-12-19T22:18:07+09:00
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参考にしたサイト
- GitHub PagesにReactアプリをデプロイする方法 https://qiita.com/yuitnnn/items/11375ea29ec023d19fdf
- React公式(INSTALLATION>新しいReactアプリを作る)
https://ja.reactjs.org/docs/create-a-new-react-app.html
環境
以下は私が行った環境です.
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-pages2.gh-pagesのインストール
以下のコマンドで
gh-pagesをインストールします。
以下のコマンドではcd react-pagesで作成したreact-pagesフォルダまで移動しgh-pagesをインストールしています。$ cd react-pages $ npm install gh-pages --save-devnpm
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へ変更する.
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をクリック.
以下のようなページが開かれていれば成功です.最後に
デプロイの意味とか全然しらない初学者(私)でもちょっと調べたらできるもんだなぁと感激しました。
積極的にいろんなことにトライしてみようと思いました。
次はReactをつかって自分のポートフォリを作りたいと思います。
- 投稿日:2019-12-19T20:52:04+09:00
「スタートアップ×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.jsとreact-intlを用いて行ったとのこと。
ゲストLT1「yarn upgaradeも怖くないテストをつくる」
ライブラリを常に最新の状態でも怖くないように、テストを継続的に行うという話でした。そのためにロジックとビューを分離させるようなコードの書き方をするなどの工夫を行っているとのこと。
ゲストLT2「俺のお気に入りhooksランキング」
- useArray
- useDebouncedQuery
- useFetch系
- useInterSectionObserver
- useToast, Drawer, ModalができるuseContext
さいごに
資料はおそらく後日イベントページに反映されるのでは?と思っています。
- 投稿日:2019-12-19T20:09:02+09:00
なぜ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億ドル相当の苦痛と損害を引き起こしたとみられる。
これら上記の3つの仮定に関して、一般的にエンジニアの皆さんには共通した意識があると思います。
仮定1について
仮定1. 各コンポーネント等に状態を持たせることは問題である
ElmというフレームワークはVueで言うところのvuex、Reactでいうところの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、はっきり言ってコードレビューの工数減るし、実行時エラー出ないし、新規開発や修正時のバグ出ないし、技術的負債がたまりにくいので競合他社は絶対に使わないでください。
- 投稿日:2019-12-19T15:51:39+09:00
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.SFCとReact.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;
ReactChildはReactElement、string、numberの union 型です。ReactNodeとあわせてみると、children として、string、number、boolean、null、およびundefinedのプリミティブ型が渡せることがわかります。ReactFragmentはReactNodeの配列型と定義されています。すると JSX のタグ構文で書くものは、ReactElementとなります。説明を省略しましたが、ReactPortalは Portals のことです。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.ReactChildrenは children を処理するためのユーティリティであり、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> );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> );
React.ElementTypeという型を使っています。この型を使うことで HTML の要素名を props に渡すことができます。上記ではanyを指定していますが、特定の要素を指定することもできます。以下では button 要素と a 要素のみ指定できるようにしています。type Props = { as: React.ElementType<JSX.IntrinsicElements["button"] | JSX.IntrinsicElements["a"]>; };props を切り替える
受け付ける props のセットを切り替えることができます。
<amp-img>のように layout 属性によってwidth / height属性の有無が切り替わるときに使えるパターンです。<amp-img>はlayout="fixed"のときにwidthとheight、layout="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> );さいごに
基礎編で Event や Form や Hooks についても書きたかったけど、アドベントカレンダーに間に合わなかった。型安全なコンポーネント作りは、極めていくと無限にやりたい事がでてきますが、基礎編程度をおさえておけば自分で調べて解決できると思います。はじめに紹介しましたが、react-typescript-cheatsheet に多くのパターンが紹介されているので、困ったときにぜひ参考にしてみてください。
- 投稿日:2019-12-19T15:23:13+09:00
ReactでuseStateを上手く扱うにはどのように書くのがベストか
Hooks を使ってコードを書いていると、
useState()の宣言が多くなってしまってごちゃごちゃしてしまうことがある。
このような問題に直面した時にどのように書いていくのが良いのだろうか。単純に書いた場合、Class Component に慣れている方は、stateをまとめて宣言した方が見栄えが良いと思われるかもしれない。
これについてReactのドキュメントのFAQを読むと、次のように書いてある。すべての state を 1 つの useState 呼び出しに含めても動作しますし、フィールドごとに別に useState を持たせることでも動作はします。しかしこれらの両極端の間でうまくバランスを取り、少数の独立した state 変数に関連する state をグループ化することで、コンポーネントは最も読みやすくなります。state のロジックが複雑になった場合は、それをリデューサで管理するか、カスタムフックを書くことをお勧めします。
つまり、基本的に少数の独立した state に分割して宣言することで、Custom Hook を作りやすくなり、リファクタリングしやすくなったり、コードが見やすくなるのである。
また、「どうにも上手くグループ分けができず、汚くなってしまった。」という時に限って
useReducer()を使って、処理をまとめるのが良いと考える。それ以外にも
useReducer()の利用場面として、以下のような場面がある。
- 複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化になる(https://ja.reactjs.org/docs/hooks-faq.html#how-to-avoid-passing-callbacks-down)
- 前回のstateに基づいて、stateを更新したいような場面(https://overreacted.io/ja/a-complete-guide-to-useeffect)
そのため、次のようなステップでコードを書いていくのが良いと思われる。
const x = useState(0);のように独立させて宣言してみるconst [position, setPosition] = useState({ x: 0, y: 0});のようにして役割ごとにまとめてみる- それでも上手くいかない場合は、
useReducer()を使うというようにしたほうが共通化しやすいと考える。メモ程度にまとめてみました!
間違っている所や他に意識しているようなことがあれば教えていただけたら嬉しいです。
- 投稿日:2019-12-19T14:31:23+09:00
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の型はこんな感じです。src/store/todos/types.tsexport 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>; }>;
TodosStateとTodosActionはReduxでも使うので「あーアレね」という感じだと思います。TodosContextTypeはReduxでいうproviderの中身に相当するもので、一旦stateをそれを変更するためのdispatchが入る…ということだけ定義しておきます。reducerを定義する
stateとactionの型が決まったので、Reducerを作っていきます。
src/store/todos/reducer.tsexport 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を実装します。
src/store/todos/actions.tsexport const addTodo = (text: string): TodosAction => ({ type: "ADD_TODO", text, }) export const toggleTodo = (id: number): TodosAction => ({ type: "TOGGLE_TODO", id, });普通ですね。
ContextProviderを定義する
やってきました。本丸です。
Storeを作成し、その状態と変更関数を子componentに配布していきます。/store/todos/index.tsxconst 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.tsxconst initialState: TodosState = []; export const TodosContext: TodosContextType = React.createContext<{ state: TodosState; dispatch: React.Dispatch<TodosAction> | Noop; }>({ state: initialState, dispatch: () => {}, });で、いま作った
Contextに渡すReducerを作ります。
useReducerはreducerと初期値を受け取ってstateとdispatchを返します。Hooksを利用したことでこれらはこのcomponentのstateとして管理されるため、Context APIをよく考えずに使うと起きる「状態更新の度に再描画が走る」リスクは心配無用だと思います。また、Context Providerは1つのstoreにつき1つ定義することになるのでどうやってもネスト地獄が発生します。変に結合させるとややこしくなるので
childrenを受け取る形で実装しておき、後でまとめるとよいかと思います。/store/todos/index.tsxconst 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を組み込んでゆきます。
src/store/index.tsxconst ContextProvider: React.FC<React.Props<{}>> = ({ children, }): JSX.Element => ( <VisibilityFilterContextProvider> <TodosContextProvider> <VisibleTodosContextProvider> {children} </VisibleTodosContextProvider> </TodosContextProvider> </VisibilityFilterContextProvider> ); export default ContextProvider;ネスト地獄です。こればかりは避けられませんが、逆にこういうAPI都合の謎構造は一箇所に固めて (ある程度) ブラックボックス化させておけるので、割り切ることもできるような気がしています。
特に言うことはないですが、後で解説するように「複数storeのstateを集約する」みたいなことをやろうとすると、ネストの順番が割と大事になってくるので注意が必要です。
最後にこれらをアプリケーションに接続します。
App.tsx的な、ルート階層に近い部分で行います。このサンプルだとsrc直下ですね。src/index.tsxconst App: React.FC = (): JSX.Element => ( <ContextProvider> <div className="container"> <TodoAdd /> <VisibilityFilterSelect /> <TodoList /> </div> </ContextProvider> ); ReactDOM.render(<App />, document.getElementById('root'));これでアプリケーションとstoreがつながりました。
いよいよ状態の読み出しと更新を行なっていきます。storeを使う
ToDoリストへの項目追加を行う
TodoAddコンポーネントを例に解説します。src/components/TodoAdd/index.tsxconst 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項目」を作っているものです。src/store/visibleTodos/index.tsxexport 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を活かしつつ拡張していくようなエコシステムが成熟してくると大分可能性はあるな…と感じます。以上です。
- 投稿日:2019-12-19T10:56:21+09:00
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}.
- 投稿日:2019-12-19T10:01:04+09:00
React 状態によってデザインの変わるボタンコンポーネント作成(CSS Module, classnames)
i-plug Advent Calendar 2019 の【19日目】の記事です
はじめまして、i-0701 です
この度 React を触ることになりまして、まずはボタンのコンポーネントを作成しよう!と思ったので、その備忘録を残します。
私が css まわりから作成していくタイプのため、今回は style 表示切り替え特化 です。(イベント処理に関しては今回書いておりません)
ちなみに使うものは
- CSS Module
- classnamesCSS 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.tsximport 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.tsximport 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;ちなみに
あとは
.module.scssで作成すると勝手に create-react-app さんがええ感じにしてくれます。(CSS modules とかのモジュール入れなくても動く)
npm run ejectしてでてきたconfig/webpack.config.jsを見ると、サポートしてるよって書いてくれてます。
今回だとの 457~490 行目あたりです。create-react-appさまさま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/classnamesCSS Module を使ってるのでこちら参照してコンポーネントを修正
Button.tsximport 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.tsxconst App: React.FC = () => { return ( <div className="App"> <Button text="ボタン" /> <Button text="ボタン" disabled={true} /> </div> ); }表示されました
左が通常ボタン、右が disabled なボタンです。
おまけ
失敗コードと感想
最初 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, });こんな↓書き方してしまったんですが……公式を読むとちゃんと書いてました
const classNameForButton = () => { if (props.disabled) { return ClassNames(styles.button, styles.disabled); } return ClassNames(styles.button); };
公式 Docs をちゃんと読もうって思いました
- 投稿日:2019-12-19T07:33:17+09:00
【業務】『何使ってもいいよ』と言われたプロジェクトに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組み換え javascriptvar 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ライフ!
- 投稿日:2019-12-19T07:21:59+09:00
Reactで(少し)動的なFormを作る
やりたいこと
- 上段のプルダウンで値を選ぶと、下段のプルダウンで選べる要素が変わる(制限される)
- ModalでFormを出して値を任意の数追加する
メモ
- なぜかFormikのvalidateField()の実行がエラーとなる(issue上がってない・・・)
完成イメージ
下記のような感じ。エリアで関西とかを選ぶと、大阪、兵庫とかしか選べなくなる。
趣味はいくつあるかわからないので、必要数入れてもらう(うらでは配列とかで持つ)。
コードはgithubにあげてみたので、動作確認してからいじったほうがわかりやすいかも。実装
正解があるわけではないが、とりあえず下記のようにしてみた。
App.jsimport 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;
- 投稿日:2019-12-19T04:37:06+09:00
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とりあえず
npm run startでReactアプリケーションを起動してみましょう。
さらに、ディレクトリを少し整頓して、src/App.jsを最小構成にしてみましょう。
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.jsimport 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
- 投稿日:2019-12-19T04:37:06+09:00
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とりあえず
npm run startでReactアプリケーションを起動してみましょう。
さらに、ディレクトリを少し整頓して、src/App.jsを最小構成にしてみましょう。
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.jsimport 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.jsimport 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.jsimport React from 'react'; const SampleComponent = (props) => { return ( <div> <h2>{props.title}</h2> <div>{props.content}</div> </div> ); } export default SampleComponent;App.jsimport 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のデータを取って、一覧表示させています。
実行結果
Reactコード
App.jsimport 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
- 投稿日:2019-12-19T01:32:57+09:00
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が使えるようになったものです。
公開されている部品を組み合わせてもいいですし、もちろん自分で部品を作ることもできます。
例えば、次のようなブログのトップページをイメージしてもらうと、
1つの記事のサマリーが
AbstractContentという部品になっていれば、 それを並べればTopページが作れます。<div className="TopPage"> <AbstractContent title={title1} .... /> <AbstractContent title={title2} .... /> <AbstractContent title={title3} .... /> </div>さらに
AbstractContentも何かの部品を組み合わせているので 次のようなツリー構造になります。部品の例としては、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というアーキテクチャが生み出されています。
参考: 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)に通知が来て、再描画される、という感じです。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です(ほかの環境でも動きそうですが確認していません)。
また、次のアプリをインストールしておく必要があります。
- Node.js
- Google Chrome
- React Developer Tools( Reactの開発にあると便利ですが、今回は使いません)
- Visual Studio Code
なお、React本体やTypescriptコンパイラは開発用フォルダにダウンロードされますので 上記以外ではOSを汚しません。(・・・はず)
開発環境
まずは開発環境を作ります。 次のコマンドで、
create-react-appというReact開発環境のテンプレートを生成するツールをダウンロードします。なお、npmはC#でいうところのNugetです。commandlinecd (適当な空フォルダー) npm install create-react-appダウンロードしたモジュールはnode_modulesフォルダに入っており、実行モジュールは
node_modules/.binにあります。
それを使って、開発環境を生成します。
第一引数はフォルダ名(およびプロジェクト名)を、第二引数の--typescriptはJavascriptではなくTypescriptを使うことを指定しています。commandlinenode_modules\.bin\create-react-app test --typescript出来上がった開発環境のフォルダに移動して、実行してみましょう。
commandlinecd test npm start初期の画面が表示されます。
npm startは、Visual Studioのデバッグの開始のようなもので、
自動的にhttpサーバーが起動し、ソースがビルドされて、Webページが表示されます。この状態で、ソースコードを一部変更してみます。
Visual Studio Code等のテキストエディタでsrc/App.tsxを編集します。src/App.tsx- Edit <code>src/App.tsx</code> and save to reload. + Hello Worldソースコードを変更すると自動でビルドされ、Webページも更新されます。
終了は、コマンドプロンプトで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ですね。必要なパッケージをダウンロードします。
commandlinenpm 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": { } }次のコマンドでソースコードのコードチェックが行われます。
commandlinenode_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のファイルを開いた場合に以下のように警告が表示され、
上書き保存時に自動修正できる箇所は修正されます。eslintの出力に記載があるように
--fixをつけると自動Fixできますが、ファイル数も少ないので、
Visual Studio Codeで1つ1つファイルを開いては上書き保存していきます。もう一度eslintを実行すると、自動解決できない警告だけになっています。
ここでは、気にしない方向で無視します。
なお、@typescript-eslint/explicit-function-return-typeを無視するようにeslintrc.jsonに記載するか、serviceWorkerの関数の戻り値をちゃんと宣言すれば、警告を消せます。commandlinenode_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で実行できるようになります。commandlinenpm 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)参考
- https://qiita.com/madono/items/a134e904e891c5cb1d20
- https://ginpen.com/2019/08/06/eslint-for-react-in-typescript/
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になります。
classはclassNameにする(javascriptの構文と名前が被るので)- Htmlの属性は文字列以外は{}で囲ってJavascriptまたはTypescriptで記載する
- styleは
{{width:"50%"}}のように{}で囲われたJsonで書く
npm startしてみると、当然ですが、見た目は変わりません。参考: https://qiita.com/daikoncl/items/a3806d8a8bf35f086487
Material-UIを導入する
Reactのデフォルトでは、標準のhtmlタグしか使えません。 デザインセンス無しでそこそこイケてるWeb画面を作るために、既存の部品を使います。
ここでは、上で紹介した、Material-UIです。まずパッケージをインストールします。
commandlinenpm 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.tsximport 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してみると次のようになります。
いろいろスタイルは手抜きですが、影とかアイコンとかそれっぽくなりました。データをサーバーから取得
現状、コンテンツは
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を入れます。commandlinenpm 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で型はチェックできていますし。)ユニットテストを実行してみましょう。
commandlinenpm 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.tsxのstoresで管理し、Appにインジェクトします。まずは必要なパッケージをインストールします。
commandlinenpm 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.tsxのstores内のappStoreがAppに渡されます。なお、当然ですが、見た目は変わりません。
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すると以下のようになります。
dummyDataをいろいろ変えてみるとWebページが変わると思います。部品を作る
Reactの部品化の練習のためにTOPページのサマリー要素部分を部品化してみます。
上と同じように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} + />相変わらず見た目は変わりませんが、
サマリー部分の部品化ができました。なお、この部品は再利用しないので、部品化はあまり意味はありません。
ページ遷移を作る
SPAといえども、普通のHtmlのようにページ遷移がしたいことが多いです。
概要←→詳細 で行き来することもありますし、1ページいろいろ押し込むと使いにくいですし。ここでは、
React-Routerを使ってページ遷移を作ります。まずページ遷移があるということはURLを設計しないといけません。
簡単ですが、以下のようにします。
http://~/→ トップページ(サマリーのページ)http://~/contents/(Id)→ 詳細ページ(Idは数字)では作ってみましょう。
まず、必要なパッケージを取得します。
commandlinenpm install react-router-dom @types/react-router-dom --save次にTOPページから詳細ページへのリンクを作ります。
React-Routerでのリンクは<Link to="url">ラベル</Link>を使いますので、
タイトル部分をリンクに置き換えます。
URLはPropsに追加して、親ページから渡してもらうことにします。src/SummaryItem.tsximport 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で連携させます。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.tsximport 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を押しても元ページに戻ったりしません。ビルドしてデプロイ可能にする
ある程度アプリができたら、ビルドしてデプロイできるようにします。
次のコマンドを実行すると、buildフォルダに成果物が出来上がります。commandlinenpm 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自分で打ち込むのが面倒な方はそちらを見てもらってもよいと思います。
個人的には写経はおすすめですが。
- 投稿日:2019-12-19T00:30:15+09:00
React-Bootstrapで色々使ってみる -Navbar-
React-Bootstrapを使えるようにするでboot-strapが使えるようになったので、色々触ってみる。今回はNavbar。
ほぼ本家のコピペなので、詳しくは本家参照。Navbar
ヘッダーにあたる部分。多分全アプリで必要だと思う。
とりあえずコピペしてみる。
App.jsimport 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;これだとスマホとか画面が小さくなったときに選択肢が多いと、画面崩れが起きてしまう。悲しい。
スマホとかでレスポンシブに動かしたいんや!!というときは、
下記のように書くといい感じにレスポンシブにしてくれるっぽい。app.jsimport 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;expand="true"にするとずっとハンバーガーメニューが表示されるようになる。
で囲った部分は、collapseされずに表示される公式が用意していない色への変更とかはそのうち書く。
- 投稿日:2019-12-19T00:23:45+09:00
勉強しながら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-app2. Todoコンポーネントを作成
ディレクトリとファイルを作成
$ cd src $ mkdir components $ cd components # Mac $ touch Todo.js # Win(PowerShell) $ New-Item -Type File Todo.jsTodoコンポーネント作成
Todos.jsimport 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.jsimport React from 'react'; import './App.css'; import Todos from './components/Todos' function App() { return ( <div className="App"> <Todos/> </div> ); } export default App;Todoの追加、実施/未実施の切り替えができる簡単なTodoアプリが作成できました。
下切れてるんですけど...
まとめ
まとめるほどの内容でもありませんがコンポーネント作成から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


































