- 投稿日:2020-05-23T22:24:56+09:00
ReactとRailsを同一サーバーで動かす方法
経緯
Railsの学習を始めるに当たって、 ERBを利用したVIEWでフロント側を構築していたが、
最近はSPAというものが流行っていると小耳に挟んだ。とりあえずチャレンジ!
ということでReactの学習を並行して進めていたが、、、
これどうやってデプロイするんだ。。。となってしまいました笑
とりあえず別サーバーで動かすことにしてCORS制約などもなんとなくクリアして無事にRailsとReactのやりとりが可能になった。
しかし、ReactとRailsを利用したアプリ関係の記事を漁っていると、同一サーバーで動かしているものを多数発見!!!
何回かチャレンジしたものの上手く行かず断念。。。しばらく時間が過ぎ、この土日に絶対動かしてやる!!! と執念で参考記事を探しまくった結果、
Heroku公式のBlogにわかりやすい記事があり、やっと構築ができました笑同じような人がいたら参考になれば良いなと思い投稿するに至りました!
前提条件
- Heroku blogの記事から必要最小限の部分だけ引用したものになります
- Ruby、Rails、Node、npm OR yarnなどの基本的なインストールが済んでいること
- Railsアプリを作成したことがある
- Reactアプリをcreate-react-appで作成したことがある
- Herokuでホスティングをしたことがある(gitでデプロイ)
参考記事:A Rock Solid, Modern Web Stack—Rails 5 API + ActiveAdmin + Create React App on Heroku
Railsアプリの作成
rails new コマンドでrails アプリを作成します。
--apiコマンドでapiモードにするのをお忘れなく
rails new rails_app --api
作成したアプリが起動するか確かめましょう。
cd rails_app rails s
Reactアプリの作成
Railsアプリのルートディレクトリ上でclientという名前でReactアプリを構築します。
npm create-react-app client
Railsと同じように、Reactが動くか確かめましょう。
cd client npm start
このままだと開発中にReactとRailsで別ポートとなるので
package.jsonに設定を追加します。package.json{ "name": "client", "version": "0.1.0", "private": true, "proxy": "http://localhost:3001", #ここを追加 ... }この設定によりRails側には、Reactアプリがポート3001で起動しているように認識させます。
React - Rails ローカル編
ローカル環境立ち上げ用のタスクを作成します。
ProcfileにReactとRailsを立ち上げるコマンドを書き、Railsのタスクで呼び出す方法にします。
Procfileはheroku上で利用されるファイルで、プロセスのタイプ:コマンドで記述できます。
<process type>: <command>今回は、ルートディレクトリにProcfile.devというファイル名で、RailsアプリとReactアプリの起動コマンドを記述します。
Procfile.devweb: cd client && PORT=3000 npm start api: PORT=3001 && bundle exec rails s呼び出しコマンドを簡潔にしたいため、taskにします。
start.rakenamespace :start do desc 'Start development server' task :dev do exec 'heroku local -f Procfile.dev' end endtaskでprocfileの起動コマンドをキックします。
rake start:dev
ReactとRailsがどちらも立ち上がったと思います。
ターミナル上のログでわかりづらい場合はRails側で何かしらJsonを返すようにしておき、React側からfetchできるようにしておくとわかりやすいと思います。
React - Rails Heroku編
メインテーマであるHeroku上へのデプロイです。
まずはルートディレクトリにpackage.jsonファイルを作成します。
RailsのpublicディレクトリにReactの静的ファイルを作成するコマンドを記述します。
package.json{ "name": "list-of-ingredients", "license": "MIT", "engines": { "node": "10.15.3", "yarn": "1.15.2" }, "scripts": { "build": "yarn --cwd client install && yarn --cwd client build", "deploy": "cp -a client/build/. public/", "heroku-postbuild": "yarn build && yarn deploy" } }production用のProcfileを作成し、railsの起動コマンドを記述します。
必要に応じてrelease後のmigrateコマンドも記述します。
Procfileweb: bundle exec rails s release: bin/rake db:migrate
CLIを利用してheroku上のdynoの作成とビルドパックの設定を行います。アプリの作成heroku apps:create
ビルドパックの設定heroku buildpacks:add heroku/nodejs --index 1 heroku buildpacks:add heroku/ruby --index 2
heroku上の準備ができたのでgitでpushします。
git add . git commit -m "React - Rails" git push heroku masterアプリを開きます。
heroku openあとがき
みなさん無事に動きましたでしょうか?
もっと良い方法があれば是非教えていただけるとありがたいです。
もし動かない場合は下記の参考元の記事を読んでみてください!
参考記事:A Rock Solid, Modern Web Stack—Rails 5 API + ActiveAdmin + Create React App on Heroku
- 投稿日:2020-05-23T18:46:46+09:00
React + Emotionで作ったアプリがGitHub Actionsでハマった
はじめに
ReactアプリにEmotionを導入して、GitHub ActionsでCI/CD環境構築しようとしたらちょっとハマってしまいました。
ハマったポイントと解決方法を記事にします。ハマったポイント
まずは私が何をしてどうハマったのか書いていきます。
使おうとしたワークフロー
私はこんなワークフローを使おうとしていました。
name: Firebase Deploy on: push: branches: - master jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@master - name: Install Dependencies run: npm install - name: Build run: npm run build env: REACT_APP_FIREBASE_API_KEY: ${{ secrets.REACT_APP_FIREBASE_API_KEY }} REACT_APP_FIREBASE_AUTH_DOMAIN: ${{ secrets.REACT_APP_FIREBASE_AUTH_DOMAIN }} REACT_APP_FIREBASE_PROJECT_ID: ${{ secrets.REACT_APP_FIREBASE_PROJECT_ID }} REACT_APP_FIREBASE_APP_ID: ${{ secrets.REACT_APP_FIREBASE_APP_ID }} REACT_APP_FIREBASE_CONTINUE_URL: ${{ secrets.REACT_APP_FIREBASE_CONTINUE_URL }} - name: Archive Production Artifact uses: actions/upload-artifact@master with: name: build path: build deploy: name: Deploy needs: build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@master - name: Download Artifact uses: actions/download-artifact@master with: name: build path: build - name: Deploy to Firebase uses: w9jds/firebase-action@master with: args: deploy env: FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}これでいけるやろ!と思っていざプッシュしたところ…
発生したエラー
ワークフローの実行結果を確認すると、ビルドが失敗していました。
Run npm run build // 中略 Treating warnings as errors because process.env.CI = true. Most CI servers set it automatically. Failed to compile. ./src/containers/loginForm.tsx Line 2:8: 'React' is defined but never used @typescript-eslint/no-unused-vars ./src/components/loginForm.tsx Line 2:8: 'React' is defined but never used @typescript-eslint/no-unused-vars『環境変数
CI
がtrue
になるからwarning
がerror
になるよ。大体のCIサーバーで自動的にこうなるよ』と言われました。
具体的なエラーは定義済のReact
が未使用というものでした(@typescript-eslint/no-unused-vars)どうしてこうなった
通常、JSX構文で記述された式は
React.createElement()
へコンパイルされるため、React
をインポートする必要がありますが、Emotionを使う場合はちょっと違います。Emotionのcss Propを使う場合、JSX Pragmaという特別なコメントをソースファイルのトップに記述する必要があります。
JSX_Pragma/** @jsx jsx */
このコメントによって
React.createElement
の代わりにEmotion独自のjsx
関数へコンパイルされるようになります。
これでcssという便利なpropが使えるようになり、JSの中でCSSを記述できるようになるわけですが、React
が使われなくなります。なのでこのままだと@typescript-eslint/no-unused-varsに抵触します。これを何とかするために.eslintrc.js
でrulesを追加します。module.exports = { // 中略 rules: { // 中略 "@typescript-eslint/no-unused-vars": [ "error", { varsIgnorePattern: "[Rr]eact", }, ], // 中略 }, // 中略 };Emotionを使う場合はやや手間をかけてあげないといけないのですが、今回はこれが原因でエラーが発生したわけです。
解消方法
解消するにあたって↓のIssueを参考に、ビルドのステップを実行する際の環境変数
CI
をfalse
に設定しました。
Stop Treating warnings as errors because process.env.CI = true. #3657修正したワークフローがこちら
name: Firebase Deploy on: push: branches: - master jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@master - name: Install Dependencies run: npm install - name: Build run: npm run build env: CI: false REACT_APP_FIREBASE_API_KEY: ${{ secrets.REACT_APP_FIREBASE_API_KEY }} REACT_APP_FIREBASE_AUTH_DOMAIN: ${{ secrets.REACT_APP_FIREBASE_AUTH_DOMAIN }} REACT_APP_FIREBASE_PROJECT_ID: ${{ secrets.REACT_APP_FIREBASE_PROJECT_ID }} REACT_APP_FIREBASE_APP_ID: ${{ secrets.REACT_APP_FIREBASE_APP_ID }} REACT_APP_FIREBASE_CONTINUE_URL: ${{ secrets.REACT_APP_FIREBASE_CONTINUE_URL }} - name: Archive Production Artifact uses: actions/upload-artifact@master with: name: build path: build deploy: name: Deploy needs: build runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@master - name: Download Artifact uses: actions/download-artifact@master with: name: build path: build - name: Deploy to Firebase uses: w9jds/firebase-action@master with: args: deploy env: FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}これで無事エラーが解消されました
参考
- 投稿日:2020-05-23T18:29:05+09:00
Reactでfullcalenderを使ってみたのでまとめてみる
はじめに
ReactでFullcalenderを使って、カレンダーアプリっぽいことをする機会があったので、使い方など書いていきます。
使ったもの
- react(v16.13.1)
- react-datepicker(v2.16.0)
- @fullcalendar/daygrid(v4.4.0)
- @fullcalendar/interaction(v4.4.0)
- @fullcalendar/react(v4.4.0)
- @fullcalendar/timegrid(v4.4.0)
作りたいもの
以下機能を入れる
* スケジュールの保存
* スケジュールの変更
* スケジュールの削除
* カレンダーを範囲選択してその時間が初期入力された入力フォームを出す
* イベントをクリックでそのイベントの内容が初期入力された変更フォームを出すデモはこちら
スマホ対応してません・・・プロジェクト作成
create-react-appでReactのプロジェクトを作成して、上記のとおり必要なパッケージをインストールします。(react以外)
create-react-app app npm install --saveカレンダー部分を作る
インポート
Fullcalenderに必要なパッケージをインポートします。
App.jsimport FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import "@fullcalendar/core/main.css"; import "@fullcalendar/daygrid/main.css"; import "@fullcalendar/timegrid/main.css";カレンダーを実装
カレンダーを表示したいところに以下置いていく。
他にもいろいろ設定できたりしますので、公式ページを参照してください。App.js<FullCalendar locale="ja" // 日本語 defaultView="timeGridWeek" // 基本UI slotDuration="00:30:00" // 表示する時間軸の最小値 selectable={true} // 日付選択可能 allDaySlot={false} // alldayの表示設定 titleFormat={{ year: "numeric", month: "short", day: "numeric", }} // タイトルに年月日を表示 header={{ left: "prev,next,today", center: "title", right: "dayGridMonth,timeGridWeek", }} businessHours={{ daysOfWeek: [1, 2, 3, 4, 5], startTime: "0:00", endTime: "24:00", }} plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]} ref={this.ref} weekends={true} // 週末表示 events={this.myEvents} // 起動時に登録するイベント select={this.handleSelect} // カレンダー範囲選択時 eventClick={this.handleClick} // イベントクリック時 />
this.myEvents
はカレンダーに表示するイベントの配列で、title
とstart
とend
があればだいたいいい感じに表示されます。
end
がない場合は1時間と範囲になります。ここでは
memo
を追加して予定に関するメモを残せるようにしています。App.jsthis.myEvents = [ { id: 0, title: "event 1", start: "2020-05-22 10:00:00", end: "2020-05-22 11:00:00", memo: "memo1", }, { id: 1, title: "event 2", start: "2020-05-23 10:00:00", end: "2020-05-23 11:00:00", memo: "memo2", }, ];入力・変更フォームを実装する
this.state.isChange
で入力or変更の切り替えをしています。
- カレンダーの範囲選択時→入力
- イベントクリック時→変更
this.state.formInview
でフォームの表示・非表示の切り替えをしています。App.jsrenderForm() { return ( <div className={ this.state.formInview ? "container__form inview" : "container__form" } > <form> {this.state.isChange === false ? ( <div className="container__form__header">予定を入力</div> ) : ( <div className="container__form__header">予定を変更</div> )} <div>{this.renderTitle()}</div> <div>{this.renderStartTime()}</div> <div>{this.renderEndTime()}</div> <div>{this.renderMemo()}</div> <div>{this.renderBtn()}</div> </form> </div> ); }フォームの中身は以下の通りになります。
- タイトル
- 開始日時
- 終了日時
- メモ
- キャンセルor削除ボタン
- 保存or変更ボタン
フォームの内容に変更があった場合に、
state
で管理しているものをonChange
で更新しています。日時部分の実装には
DatePicker
を使用しています。開始日時が終了日時より遅くなったときに終了日時を開始日時に合わせるみたいな処理をいれたほうがいいですが、とりあえずこんな感じで。。App.jsrenderTitle() { return ( <React.Fragment> <p className="container__form__label">タイトル</p> <input className="container__form__title" type="text" value={this.state.inputTitle} onChange={(e) => { this.setState({ inputTitle: e.target.value }); if (e.target.value === "") { this.setState({ isInputTitle: false }); } else { this.setState({ isInputTitle: true }); } }} /> </React.Fragment> ); } renderMemo() { return ( <React.Fragment> <p className="container__form__label">メモ</p> <textarea className="container__form__memo" rows="3" value={this.state.inputMemo} onChange={(e) => { this.setState({ inputMemo: e.target.value }); }} /> </React.Fragment> ); } renderStartTime() { return ( <React.Fragment> <p className="container__form__label">開始日時</p> <DatePicker className="container__form__datetime" locale="ja" dateFormat="yyyy/MM/d HH:mm" selected={this.state.inputStart} showTimeSelect timeFormat="HH:mm" timeIntervals={10} todayButton="today" onChange={(time) => { this.setState({ inputStart: time }); }} /> </React.Fragment> ); } renderEndTime() { return ( <React.Fragment> <p className="container__form__label">終了日時</p> <DatePicker className="container__form__datetime" locale="ja" dateFormat="yyyy/MM/d HH:mm" selected={this.state.inputEnd} showTimeSelect timeFormat="HH:mm" timeIntervals={10} todayButton="today" onChange={(time) => { this.setState({ inputEnd: time }); }} /> </React.Fragment> ); } renderBtn() { return ( <div> {this.state.isChange === false ? ( <div> <input className="container__form__btn_cancel" type="button" value="キャンセル" onClick={() => { this.setState({ formInview: false }); }} /> <input className="container__form__btn_save" type="button" value="保存" disabled={!this.state.isInputTitle} onClick={this.onAddEvent} /> </div> ) : ( <div> <input className="container__form__btn_delete" type="button" value="削除" onClick={this.onDeleteEvent} /> <input className="container__form__btn_save" type="button" value="変更" onClick={this.onChangeEvent} /> </div> )} </div> ); }カレンダー範囲選択時の処理を実装する
カレンダーの範囲選択時に指定した時間で入力フォームを初期表示するために、
state
で管理しているタイトル・開始日時・終了日時・メモの値を更新します。App.jshandleSelect = (selectInfo) => { let start = new Date(selectInfo.start); let end = new Date(selectInfo.end); start.setHours(start.getHours()); end.setHours(end.getHours()); this.setState({ inputTitle: "" }); this.setState({ inputMemo: "" }); this.setState({ isInputTitle: false }); this.setState({ inputStart: start }); this.setState({ inputEnd: end }); this.setState({ isChange: false }); this.setState({ formInview: true }); };イベントクリック時処理を実装する
イベントクリック時に保存されている値で変更フォームを初期表示するために、
state
で管理しているタイトル・開始日時・終了日時・メモの値を更新します。App.jshandleClick = (info) => { this.selEventID = info.event.id; const selEvent = this.myEvents[info.event.id]; const title = selEvent.title; const memo = selEvent.memo; const start = new Date(selEvent.start); const end = new Date(selEvent.end); this.setState({ inputTitle: title }); this.setState({ inputMemo: memo }); this.setState({ isInputTitle: true }); this.setState({ inputStart: start }); this.setState({ inputEnd: end }); this.setState({ isChange: true }); this.setState({ formInview: true }); };追加時の処理を実装する
登録したいイベントを
event
に一旦いれていき、addEvent
で登録します。App.jsonAddEvent() { const starttime = this.changeDateToString(this.state.inputStart); const endtime = this.changeDateToString(this.state.inputEnd); if (starttime >= endtime) { alert("開始時間と終了時間を確認してください。"); return; } const event = { title: this.state.inputTitle, memo: this.state.inputMemo, start: starttime, end: endtime, }; if (this.addEvent(event) === true) { window.alert("設定しました"); this.setState({ formInview: false }); } }
this.myEvents
にpushして、表示も更新するために、this.ref.current.getApi().addEvent(ev);
を呼びます。App.jsaddEvent = (ev) => { ev.id = this.getID(); this.myEvents.push(ev); this.ref.current.getApi().addEvent(ev); return true; };いい感じにidを取得できる関数たち
App.jssortEventID = (a, b) => { return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; }; getID = () => { this.myEvents.sort(this.sortEventID); let i; for (i = 0; i < this.myEvents.length; i++) { if (this.myEvents[i].id !== i) { break; } } return i; };いい感じに時間表示を変換する関数たち
App.jschangeDateToString(dt) { const year = dt.getFullYear(); const month = this.getdoubleDigestNumer(dt.getMonth() + 1); const date = this.getdoubleDigestNumer(dt.getDate()); const hour = this.getdoubleDigestNumer(dt.getHours()); const minutes = this.getdoubleDigestNumer(dt.getMinutes()); const retDate = `${year}-${month}-${date} ${hour}:${minutes}:00`; return retDate; } getdoubleDigestNumer(number) { return ("0" + number).slice(-2); }変更時の処理を実装する
変更したいイベントを
event
に一旦いれていき、changeEvent
で変更します。App.jsonChangeEvent(values) { if (window.confirm("変更しますか?")) { const starttime = this.changeDateToString(this.state.inputStart); const endtime = this.changeDateToString(this.state.inputEnd); if (starttime >= endtime) { alert("開始時間と終了時間を確認してください。"); return; } const event = { title: this.state.inputTitle, memo: this.state.inputMemo, start: starttime, end: endtime, id: this.selEventID, }; if (this.changeEvent(event) === true) { window.alert("イベントを変更しました。"); this.setState({ formInview: false }); } } else { return; } }
this.myEvents
の指定したidの内容を書き換えて、一旦表示を消してから再登録します。App.jschangeEvent = (ev) => { this.myEvents[ev.id].title = ev.title; this.myEvents[ev.id].memo = ev.memo; this.myEvents[ev.id].start = ev.start; this.myEvents[ev.id].end = ev.end; this.ref.current.getApi().getEventById(ev.id).remove(); this.ref.current.getApi().addEvent(this.myEvents[ev.id]); return true; };削除時の処理を実装する
削除したいイベントに
isDel = true
を設定しています。一応DB連携した時に何を削除したかをわかるようにthis.myEvents
から削除しないようにしています。App.jsonDeleteEvent() { if (window.confirm("削除しますか?")) { if (this.selEventID < this.EVENT_SEL_NON) { let EventID = this.selEventID; let delevent = this.ref.current.getApi().getEventById(EventID); delevent.remove(); this.selEventID = this.EVENT_SEL_NON; this.myEvents[EventID].isDel = true; } this.setState({ formInview: false }); alert("イベントを削除しました。"); } else { return; } }まとめ
これでだいたいできたと思います。
入力フォーム表示時に他をクリックすると消えたり、スタイル調整とかは割愛します。
DB連携とログイン機能を入れるだけで簡単にそれなりのカレンダーアプリができてしまいますので結構いいですね!ソース全文はGitHubにありますので、よろしければ確認してみてください。
間違っていたり、こうしたほうがいいよとかありましたら、コメントお願いします!!
参考にしたもの
- 投稿日:2020-05-23T17:23:19+09:00
es-lintでソースの自動整形を適用させる方法(vs code) 自分用メモ
教えてもらった内容のメモなのでこれだけで動作するかはわかりません、、
あとで検証して更新するのでゆるしてくださいes-lintの公式でだいたいは書いてある?
プラグインの導入
今回はes-lintとprettierを併用して行きたいと思います。
下記のコマンドを実行します。
以下の2つはes-lintとprettierのフォーマットを競合させないように必要らしい
- eslint-config-prettier
- eslint-plugin-prettier
$ yarn add eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-react prettier -Dソースコード側の設定
.vscodeディレクトリ内にextensions.jsonを用意します。
extensions.json{ "recommendations": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", ] }.vscodeディレクトリ内settings.jsonに以下を追記します。
settings.json{ "eslint.packageManager": "yarn", "eslint.workingDirectories": [ { "directory": "./app", "changeProcessCWD": true } ], "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, }eslintrc.jsと.prettierrcの設定は下記の通り、、
最低限の条件で適用しているのでその他のルールを追加したい場合はextendsに追記していくといいそうです。rulesにはignoreする条件等を書きますがこちらは最低限にしないとlintの意味がなくなってしまうのでなるべく追加しないようにしたい。
eslintrc.jsmodule.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'prettier/@typescript-eslint', 'prettier/react', ], plugins: ['@typescript-eslint', 'react', 'prettier'], env: { node: true, es6: true }, parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', ecmaFeatures: { jsx: true, }, }, rules: { 'react/prop-types': 'off', '@typescript-eslint/explicit-function-return-type': 0, 'no-restricted-imports': [ 'error', { patterns: ['../'], }, ], }, settings: { react: { version: 'detect', }, }, };.prettierrc{ "eslint.packageManager": "yarn", "eslint.workingDirectories": [ { "directory": "./app", "changeProcessCWD": true } ], "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, }以上でできるはず...!
- 投稿日:2020-05-23T17:05:37+09:00
react-big-calendar
公式
http://jquense.github.io/react-big-calendar/examples/index.html
Is 何
以下のようなイベントカレンダーを作れる。
fullcalendarに影響を受けている。つまりこれの後発。月
週
work 週
日
一覧
アーキテクチャ
Calendar
カレンダー本体。
Calendarタグのプロパティで諸々の設定を行う。Event
カレンダー内の予定をEventと呼んでいる。
以下の定義をデフォルトとしている。
デフォルトと書いたのは、APIからアクセスする際のキーを上書き指定できるため。
これにより、Eventのプロパティ名も自在(合わせてアクセスキーを指定する必要はある)。Event { title: string, start: Date, end: Date, allDay?: boolean resource?: any, }Resource
スケジュールを持つ主体。ユーザーだったり、施設だったり。
公式Documentに説明はない。が、触発されたと記載があるfullcalendar.ioのドキュメントのResourceを読むと概念的に整合する。(怒
EventにはResourceを紐付けることができる。
リソース特定方法は、resourceAccessorがある。
デフォルトはDocumentのとおり、resourceId。
EventにresourceIdプロパティを加えればすぐ使える。Resourceを定義すると、この画面のように上部にResource.titleが表示される。
Localization and Date Formatting
日付のフォーマットとローカライゼーションが外部化されている。
momentが利用できる。カスタマイズ性
Documentにも説明はあるが、Calendarのプロパティ「component」にカスタマイズした描画コンポーネント(jsx)を渡せる。
- 渡し方
let components = { event: MyEvent, // used by each view (Month, Day, Week) toolbar: MyToolbar, agenda: { event: MyAgendaEvent // with the agenda view use a different component to render events } } <Calendar components={components} />
- 再定義可能なものの全量(@typesから抜いた)
どこまでカスタマイズできるかは見ていない。
export interface Components<TEvent extends object = Event> { event?: React.ComponentType<EventProps<TEvent>>; eventWrapper?: React.ComponentType<EventWrapperProps<TEvent>>; eventContainerWrapper?: React.SFC | React.Component | React.ComponentClass | JSX.Element; dateCellWrapper?: React.SFC | React.Component | React.ComponentClass | JSX.Element; timeSlotWrapper?: React.SFC | React.Component | React.ComponentClass | JSX.Element; timeGutterHeader?: React.SFC | React.Component | React.ComponentClass | JSX.Element; timeGutterWrapper?: React.SFC | React.Component | React.ComponentClass | JSX.Element; toolbar?: React.ComponentType<ToolbarProps>; agenda?: { date?: React.SFC | React.Component | React.ComponentClass | JSX.Element; time?: React.SFC | React.Component | React.ComponentClass | JSX.Element; event?: React.SFC | React.Component | React.ComponentClass | JSX.Element; }; day?: { header?: React.SFC | React.Component | React.ComponentClass | JSX.Element; event?: React.SFC | React.Component | React.ComponentClass | JSX.Element; }; week?: { header?: React.SFC | React.Component | React.ComponentClass | JSX.Element; event?: React.SFC | React.Component | React.ComponentClass | JSX.Element; }; month?: { header?: React.SFC | React.Component | React.ComponentClass | JSX.Element; dateHeader?: React.SFC | React.Component | React.ComponentClass | JSX.Element; event?: React.SFC | React.Component | React.ComponentClass | JSX.Element; }; /** * component used as a header for each column in the TimeGridHeader */ header?: React.ComponentType<HeaderProps>; resourceHeader?: React.ComponentType<ResourceHeaderProps>; }
- 投稿日:2020-05-23T15:50:07+09:00
Gatsbyサイトの動的ページにコメント欄機能を:Desqus設置
Gatsbyサイトの動的ページにコメント欄機能を:Desqus設置
簡単ではあるが、少し手間取ったので備忘録。
Disqus登録
サインアップしてログインするとこのページに。
I want install disqus on my siteをえらぶ。
複数サイトを持ってる場合はメニューで目的のサイトをクリックし
右端のEdit Settingをクリックすると、設定ページに飛ぶ。
Disqusインストールと設定・設置
gatsby-plugin-disqus
とdisqus-react
とある。両方試したがdisqus-reactの方が安定して素直に設置できたのでそちらを。インストール
$ yarn add disqus-react
global-config.js
いじらない。
記事表示ファイルarticle.jsの編集。
gatsby-plugin-disqusをインポート
// templates/article.js import { DiscussionEmbed } from "disqus-react";そして/* コメント欄機能Disqusの設定 */ のところを追加。
// templates/article.js import React from "react" import { graphql } from "gatsby" import Layout from "../components/layout" import SEO from "../components/seo" import { Container, Row, Col, Breadcrumb} from 'react-bootstrap' import { useLocation } from "@reach/router" import { DiscussionEmbed } from "disqus-react"; const Articlearticle = props => { const article = props.data.microcmsArticles // ㊟allMicrocmsArticleでない /* コメント欄機能Disqusの設定 */ const slug = useLocation() const title = article.title const disqusShortname = "bij-xxxx-x"; const disqusConfig = { config: { identifier: slug, title }, } ・・・略・・・
- slugはuseLocation()でURLパスを持ってきて格納。
- titleはmicrocmsArticlesのGraphQLから持ってきて格納。
- disqusShortnameはDisqusコンソールに表示されている。
DiscussionEmbed埋め込み
次のように、レンダーしたいところにshortnameとconfigのプロパティをつけて埋め込む。
<DiscussionEmbed shortname={disqusShortname} config={disqusConfig} />コード全容
いちおうarticle.jsのコード全文
// templates/article.js import React from "react" import { graphql } from "gatsby" import Layout from "../components/layout" import SEO from "../components/seo" import { Container, Row, Col, Breadcrumb} from 'react-bootstrap' import { useLocation } from "@reach/router" import { DiscussionEmbed } from "disqus-react"; const Articlearticle = props => { const article = props.data.microcmsArticles // ㊟allMicrocmsArticleでない /* コメント欄機能Disqusの設定 */ const slug = useLocation() const title = article.title const disqusShortname = "bij-site-2"; const disqusConfig = { config: { identifier: slug, title }, } return ( <Layout> <Container fluid="md"> <SEO title={article.title} description={sumarrize(article.body)} image={article.pict.url} lang="ja" /> <div> <h1 style={{ fontSize: `1.25rem`}}>{article.title}</h1> <span style={{ fontSize: `1.1rem`}} dangerouslySetInnerHTML={{ __html: `${article.title_origin}`, }} > </span> <Row> <Col md={8}> <span style={{ fontSize: `0.9rem`, color: `gray` }}>著者:{article.writer.name}</span> </Col> <Col md={4}> <span style={{ fontSize: `0.9rem`, color: `gray` }}>投稿:{article.date}</span> </Col> </Row> <br /> <p dangerouslySetInnerHTML={{ __html: `${article.body}`, }} ></p> <br /> <span>著者:{article.writer.name}</span> <br /> <img src={article.writer.image.url} width={160} alt={article.writer.name} /> <p dangerouslySetInnerHTML={{ __html: `${article.writer.profile}`, }} ></p> </div> <DiscussionEmbed shortname={disqusShortname} config={disqusConfig} /> </Container> </Layout> ) } export default Articlearticle export const query = graphql` query($id: String!) { microcmsArticles(id: { eq: $id }) { title title_origin date body pict { url } body category { name } writer { name profile image { url } } } } ` /* <SEO description= />用に記事本文を120字以内に収める関数 */ let striptags = require('striptags'); function sumarrize(html) { const metaDescription = striptags(html).replace(/\r?\n/g, '').trim(); return metaDescription.length <= 120 ? metaDescription : metaDescription.slice(0, 120) + '...'; }
ページネーションなどもできるようだがそれは追々。
Disqusコンソール
コメントはDisqusコンソールで確認して削除したりできる。
https://bij-xxxx.disqus.com/admin/moderate/approved
無料トライアル1か月が終了したら?
Nakamuさんにコメント欄で聞いたところ、そのまま使えてるようです。
参考:
Adding Comments
Add Disqus comments to a Gatsby blog
Gatsby Plugin Disqus
What platform is your site on?
Universal Embed Code
GatsbyJSで構築したブログにDISQUSのコメントフォームを設置する方法
- 投稿日:2020-05-23T14:16:11+09:00
ReasonReact から Recoil を使う
ReasonReact から Recoil を使う
Facebook が新しい React 用の状態管理ライブラリを公開しました。 Recoil という名前です(ググラビリティ〜)。
というわけで、これを ReasonReact から使ってみます。利用するライブラリのご紹介
知っている方は飛ばして結構です。
Recoil
Facebook 発の React 用状態管理ライブラリ。
状態管理を、 Atom という状態と Selector という変換処理(純粋関数……って書いてあるんですけど普通に副作用を含める事を想定しているように見えますね?)で行います。この、 Atom から Selector を通してコンポーネントまでデータを持ってくる一連の流れを「データフローグラフ」と表現しているようです。
状態の名前が Atom というのは何となく Clojure を思い出しますね。Atom は React Hooks の useState に似ていますね。ただし、 Recoil の仕組みを利用して同じ状態を複数のコンポーネントから利用できます。
Selector は、 SQL のビューみたいな感じですね。更新処理を持たせる事ができるのも面白い。Reason
皆さんご存知、 Facebook が提供している OCaml ベースの AltJS ですね。 BuckleScript という、見た目そのまま OCaml の AltJS を、文法のみ JavaScript 風味にしたもので、 AST レベルでは完全に OCaml なのが特徴です。
強い型安全性、高いインターオペラビリティ、 JavaScript に変換された後の効率の良さが特徴とされています。特に JavaScript との連携の容易さは強く意識されており、コンパイルされた後の JavaScript コードの可読性や、利用性の高さに注意を払って開発されている点が強みです。最近の改善で、 OCaml のレコードを JavaScript のオブジェクトにマッピングするようになり、 JavaScript との連携はより強力になりました。
// 型定義 type user = { id: int, name: string, }; let user1 = {id: 42, name: "John"}; let user2 = {id: 43, name: "Alice"}; // 関数定義 // 関数ヘッドでのパターンマッチと分解が可能。 // 文字列補間も利用できる(ちょっと冗長な書き方が必要だが……)。 let displayUser = ({id, name}) => {j|[$(id)] $(name)|j};このコードが次のように変換されます(コメント部分は筆者。全体的に、ちょっと省略して順番をいじっています)。
// 型定義はコンパイル後には消えている。 OCaml は実行時に型情報を持たない。 // OCaml のレコードの定義が JavaScript のオブジェクトの定義で表現されている! var user1 = { id: 42, name: "John" }; var user2 = { id: 43, name: "Alice" }; // ES5 互換の JavaScript を吐き出すので、アロー演算子や分割代入や文字列補間は利用されない。 function displayUser(param) { return "[" + (String(param.id) + ("] " + (String(param.name) + ""))); }すごい。
ReasonReact
React の Reason 向けポーティングです。
同じ Facebook だけあってか React の中の人が開発に関わるなどしており、コミュニティもなかなか活発で、見ていて楽しいです。
関数型言語と React との相性の良さを味わってください。Recoil を ResonReact から利用する
ではその Recoil を、 ReasonReact から使ってみます。
Reason で JavaScript のライブラリを利用するのは非常に簡単なのですが、静的型付き言語なので、型定義を書いてやる必要があります。
実は Recoil の型定義については、既に @bloodyowl さんという方が書いていらっしゃるので、これを利用するのが早いでしょう。が、今回は型定義を書く練習も兼ねて、必要な部分のみを自分で実装してみる事にします。
新しいライブラリに慣れるためにそのライブラリの型定義を書いてみるというのは、用意された関数に知悉するためにも良い行いかと思います。Recoil の型定義
書いていきましょう。
Reason から JavaScript の関数や値を呼び出す時の型定義については、ここを参考にします。
Recoil.re
という名前のファイルを作り、そこに定義を書いていきます。Recoil の型
まず、状態の型を作ります。この状態というのは、
- atom
- 書き込み可能な selector
のどちらかです。
本来は、 selector が書き込み可能か不可能かをきっちりと型で表現するべきではありますが、今回は簡便さを優先してそこを無視します。
以下のような定義になります。type t('a)型引数を一つだけ取る
t
という型を定義します。その型が実際どういうものなのかは、一切書く必要がありません。RecoilRoot
次に、 RecoilRoot コンポーネントを定義します。
module Root = { [@react.component] [@bs.module "recoil"] external make: (~children: React.element) => React.element = "RecoilRoot"; };外部ライブラリのコンポーネント導入はこの辺りを参考にしました。
ここでも、本来は initializeState という prop を取るのですが、今回は省略しています。atom
そして、 atom を定義します。
type atomOption('a) = { key: string, default: 'a, }; [@bs.module "recoil"] external atom: atomOption('a) => t('a) = "atom"; let atom = (~key, ~default) => atom({key, default});atom の定義はこの辺りを参考にしました。
同名の関数を定義して、この二つのフィールドはラベル付き引数として取るようにしています。
(別に直接オブジェクトを引数に取っても良いのですが、ラベル付き引数の方がより「らしく」見えるのでこう書きました。)selector
その次に selector を定義します。
type getter = {get: 'a. t('a) => 'a}; type selectorOption('a) = { key: string, get: getter => 'a, }; [@bs.module "recoil"] external selector: selectorOption('a) => t('a) = "selector"; let selector = (~key, ~get) => selector({key, get});今回は書込み可能な selector は無視して、読み取り専用の selector の型定義のみを作ります。
同じく定義はこの辺りを参考にしています。
getter の型定義の'a.
は多相型注釈です。 この getter は利用される時に、同じ関数内で'a
の型が異なる場合が考えられる為、特定の型に固定しておく事ができません。だから、多相型注釈を使う必要があったんですね。
この辺りは @bloodyowl さんのコードを参考にしました。Hooks
最後に Hooks を定義しましょう。
[@bs.module "recoil"] external useRecoilState: t('a) => ('a, ('a => 'a) => unit) = "useRecoilState"; [@bs.module "recoil"] external useRecoilValue: t('a) => 'a = "useRecoilValue"; type setter('a) = 'a => 'a; [@bs.module "recoil"] external useSetRecoilState: t('a) => setter('a) = "useSetRecoilState";この辺りは特に説明することは無いですね。
公式サイトの Intro to External に書いてある通りにするだけです。実装
では、 Recoil を利用する実装を書いていきましょう。
今回は、 Recoil の公式サイトに載っている小さなデモを実装してみます。
テキストボックスに文字列を入力すると、それと同じ文字列と、文字数とを表示してくれるというやつですね。atom
atom 関数を使って状態を作りましょう。
let textState = Recoil.atom(~key="textState", ~default="");特に型注釈はありませんが、きちんと
Recoil.t(string)
型であると推論されます。selector
同じく、 selector 関数で selector を作ります。
let charCountState = Recoil.selector( ~key="charCountState", ~get=({get}) => get(textState) |> String.length );こちらも特に型注釈無く、
Recoil.t(int)
型であると推論されます。コンポーネント
では、コンポーネントを作って上の Recoil の atom や selector を使ってみましょう。
まず、文字列を入力するコンポーネントです。module Input = { [@react.component] let make = () => { let (textValue, setNameValue) = Recoil.useRecoilState(textState); let onChange = React.useCallback(e => setNameValue(e->ReactEvent.Form.target##value)); <div> <input value=textValue onChange /> </div>; }; };次に、入力された文字列と、その文字数とを表示するコンポーネント。
module Show = { [@react.component] let make = () => { let textValue = Recoil.useRecoilValue(textState); let charCountValue = Recoil.useRecoilValue(charCountState); <> <div> {j|Echo: $(textValue)|j}->React.string </div> <div> {j|Character Count: $(charCountValue)|j}->React.string </div> </>; }; };これらのコンポーネントを同一の RecoilRoot コンポーネントの中に入れる事で、二つの異なるコンポーネントの間で状態を共有する事ができます。
[@react.component] let make = () => <Recoil.Root> <Input /> <Show /> </Recoil.Root>;これで、 ReasonReact から Recoil を使う事ができました。
コードの全容はここに置いてあります。感想と注釈
後半、ほとんど説明が無かった事に気付かれたかと思います。
何故かと言うと、簡単で、 説明するべき事が殆ど無い からです。
細かい文法の差異を除けば、 JavaScript とほぼ同じような書き方で使う事ができます。何を解説しろというのか、という話ですね。このように、 Recoil は ReasonReact から全く違和感なく利用できます。
ただし、今回解説したのは本当に Recoil の上辺の部分だという点には注意してください。
Recoil には非同期処理を扱う為の機能なども含まれていますが、今回はそれに関しては全く触れていません。この辺りを ReasonReact で利用するとなると、 async/await を用いて書かれた Recoil のコードとはかなり異なった見た目になる事は間違いないでしょう。機能としては申し分なく利用できるはずですが。また、 Recoil も開発中のライブラリなので、 API は今後も変更されていくかもしれません。大規模に利用するのはまだ得策では無いかもしれませんね。
とはいえ、 Reason も Recoil もとても面白いので、この記事を読んで興味を持てるのであれば、是非とも触ってみてください。
- 投稿日:2020-05-23T13:33:18+09:00
Reactチュートリアルを噛み砕く
できる限り丁寧にReactチュートリアルの内容をまとめました。
なお、こちらの記事は完全なるHYOP(独りよがりアウトプット)であるということを、はじめにご承知いただきたく存じます。*本記事ではcloud9を用いて開発を進めていきます*
※一部、オリジナルの内容を変更しています。初期設定
まずはnodeの確認
ec2-user:~/environment(master)$ node -v v14.2.0react-boardgameという名前でプロジェクトを作成します。
ec2-user:~/environment(master)$ npx create-react-app react-boardgame作成したプロジェクト内の、srcディレクトリにあるソースファイルを削除します。
ec2-user:~/environment (master) $ cd react-boardgame/ ec2-user:~/environment/react-boardgame (master) $ cd src ec2-user:~/environment/react-boardgame/src (master) $ rm -f * ec2-user:~/environment/react-boardgame/src (master) $ cd .. ec2-user:~/environment/react-boardgame (master) $srcディレクトリ配下に、新たに index.css、index.js を追加します。
ec2-user:~/environment/react-boardgame (master) $ touch src/index.css src/index.jsそれぞれのファイルに記述するコードを下に記します。
index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; class Square extends React.Component { render() { return ( <button className="square"> {/* TODO */} </button> ); } } class Board extends React.Component { renderSquare(i) { return <Square />; } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } } class Game extends React.Component { render() { return ( <div className="game"> <div className="game-board"> <Board /> </div> <div className="game-info"> <div>{/* status */}</div> <ol>{/* TODO */}</ol> </div> </div> ); } } // ======================================== ReactDOM.render( <Game />, document.getElementById('root') );index.cssbody { font: 14px "Century Gothic", Futura, sans-serif; margin: 20px; } ol, ul { padding-left: 30px; } .board-row:after { clear: both; content: ""; display: table; } .status { margin-bottom: 10px; } .square { background: #fff; border: 1px solid #999; float: left; font-size: 24px; font-weight: bold; line-height: 34px; height: 34px; margin-right: -1px; margin-top: -1px; padding: 0; text-align: center; width: 34px; } .square:focus { outline: none; } .kbd-navigation .square:focus { background: #ddd; } .game { display: flex; flex-direction: row; } .game-info { margin-left: 20px; }下記のコマンドを実行し、サーバーを立ち上げます。
ec2-user:~/environment/react-boardgame (master)$ npm startエディタ上部にあるPreviewタブからPreviewRunningApplicationを選択し、
このような画面が表示されればOK。ところで、Reactとは何か。
React はユーザインターフェイスを構築するための、宣言型で効率的で柔軟な JavaScript ライブラリです。複雑な UI を、「コンポーネント」と呼ばれる小さく独立した部品から組み立てることができます。
(公式ドキュメントから引用)とのことです。
class コンポーネント名 extends React.Componentから始まる部分がコンポーネントに当たります。
今回のコードで言うとSquare Board Gameの三つです。データの流れとしては
【親】Game〜>Board〜>Square【子】
となり、この過程で、Propsというパラメーターが親コンポーネントから子コンポーネントに受け渡されています。抽象的な例えかもしれませんが、
Squareコンポーネントは、二次元的です。「一つ一つのマスにどんな値を示すか」にフォーカスしています。
Boardコンポーネントは、三次元的です。奥行きを持って盤面の全体を俯瞰し、Squareに対して表示するべきデータを渡します。
Gameコンポーネントは、そこに時間軸が加わった四次元的な存在です。ゲーム全体の情報を記憶し、それぞれの着手に対応した盤面の情報をBoardに渡します。文章だけではアレなので、実際に動かしていきましょう。
データを Props 経由で渡す
BoardコンポーネントのrenderSquareメソッド内で、Squareコンポーネントにvalueという名前のpropsを渡します。
index.jsclass Board extends React.Component { renderSquare(i) { return <Square value={i} />; } }そして、Squareコンポーネントのrenderメソッド内で、渡された値を受け取るため{/* TODO */}を{this.props.value} に書き換えます。
index.jsclass Square extends React.Component { render() { return ( <button className="square"> {this.props.value} </button> ); } }インタラクティブなコンポーネントを作る
Squareコンポーネントをクリックした際に、'X'が表示されるように書き換えていきます。
それに際して、Squareコンポーネントに自分がクリックされたことを "覚えさせる" 必要がありますね。
コンポーネントが何かを「覚える」ためには、state というものを使います。index.jsclass Square extends React.Component { ① constructor(props){ super(props); this.state={ value: null, }; } render() { return ( <button className="square" ② onClick={() => this.setState({value: 'X'})} > ③ {this.state.value} </button> ); } }変更点は3点
1. コンストラクタの追加
2. renderメソッドのonClickプロパティ
3. {this.props.value} => {this.state.value} の書き換え
です1.のステップを経て、コンストラクタ内でthis.stateに初期値を定義します。
(参考 コンストラクタとは:https://wa3.i-3-i.info/word13646.html)2.のステップで、Squareコンポーネントがクリックされた際に、this.stateの値が'X'に置き換わるように変更を加えます。
3.のステップで、マスの中にthis.stateの値が表示されるように設定しています。
また、constructorメソッドの中でsuper(props)を指定しますが、
なぜこれを書くのかはこちらの記事を参考にしてみてください。(翻訳してくださりありがとうございます。)State のリフトアップ
ゲームの基本的な部品が揃いました。完全に動作するゲームにするためには、盤面に “X” と “O” を交互に置けるようにすることと、どちらのプレーヤが勝利したか判定できるようにする必要があります。
現状、各々のsquareが「null」であるべきか「X」であるべきかを、自身で判断しています。しかしこれでは勝敗はわかりません。
勝敗を判断するためには、盤面全体の状態を認識する鳥の目、つまり、Boardコンポーネントが必要になります。方法として、BoardからSquareに対して現状の値を問い合わせるという方法もある(らしい)のですが、コードが壊れやすくわかりづらいものになってしまうのです。
なのでここでは、Boadsがマスの状態を管理し、それをpropsで各Squareに渡すことで、ゲームの管理をすることとします。複数の子要素からデータを集めたい、または 2 つの子コンポーネントに互いにやりとりさせたいと思った場合は、代わりに親コンポーネント内で共有の state を宣言する必要があります。親コンポーネントは props を使うことで子に情報を返すことができます。こうすることで、子コンポーネントが兄弟同士、あるいは親との間で常に同期されるようになります。
(公式ドキュメントから引用)まずはBoardコンポーネントに変更を加えましょう。
index.jsclass Board extends React.Component { ⬇︎ステップ①コンストラクタを追加 constructor(props){ super(props); this.state = { squares: Array(9).fill(null), }; } ⬇︎ステップ③handleClickメソッドを追加 handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } ⬇︎ステップ②propsの内容を変更 renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }順番に見ていきましょう。まずはコンストラクタについてです。
this.stateの中身に注目してください、index.js ステップ①constructor(props){ super(props); this.state = { squares: Array(9).fill(null),⬅︎ココ }; }squaresという名の、9つのnull要素を持つ配列を生成しています。
Squaresにあったコンストラクタとやっていることは一緒ですね。続いて、renderSquareメソッドの変更を確認していきます。
index.js ステップ②renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); }ここで指定しているのは、Squareコンポーネントに実際に渡すpropsの値でしたね。
valueには、引数をインデックス番号としてsquaresから取り出した値を、
onClickには、BoardコンポーネントのhandleClickメソッドを指定しています。そのhandleClickメソッドがこちら。
index.js ステップ③handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); }まず初めに、先ほどコンストラクタで生成した配列のコピーを作成しています(理由は後述)。
次に、クリックされたマスに対応する要素の値を、'X'に変更。
そしてstate.squaresを、値の変更が加えられたコピー配列である(const)squaresに置き換えています。さて、この時点でSquareコンポーネントは、
"クリックされたらそれに反応してBoard内のhandleClickメソッドを呼び出す&props.valueを表示する"
だけの部品になりました、彼はもう自分で値を保持するような存在ではありません。
ここまできたらSquareコンポーネントのコンストラクタは削除しても大丈夫です。
また、renderメソッドのみを有して、自身のstateを持たないコンポーネントは、「関数コンポーネント」と呼ばれるものに置き換えられるので、こちらも書き換えましょう。以下、現段階でのSquare/Boardコンポーネントです。
index.jsfunction Square(props) { return ( <button className="square" onClick={props.onClick}> {props.value} </button> ); } class Board extends React.Component { constructor(props){ super(props); this.state = { squares: Array(9).fill(null), }; } handleClick(i) { const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } renderSquare(i) { return ( <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)} /> ); } render() { const status = 'Next player: X'; return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }手番の処理
ここでは、Squareをクリックした際に、“X”と “O” が交互に出てくるように改良を加えます。
デフォルトでは、先手を“X”し指定します。Board のコンストラクタで state の初期値を変えればこのデフォルト値は変更できます。
index.jsclass Board extends React.Component { constructor(props){ super(props); this.state = { squares: Array(9).fill(null), xIsNext: true, ⬅︎ xIsNextを追加し、初期値にtrueを設定。 }; } handleClick(i) { const squares = this.state.squares.slice(); squares[i] = this.state.xIsNext ? 'X' : 'O'; ⬅︎ xIsNextの真偽によって表示する値を変更 this.setState({ squares: squares, xIsNext: !this.state.xIsNext, ⬅︎ stateの値を反転 }); }まず、BoardのコンストラクタにxIsNextという名の真偽値を設定し、初期値をtrueにします。
その次に、handleClickメソッド内で、Squareに渡すprops.valueに、xIsNextがtrueなら'X'を、falseなら'O'を代入します。
そのあとで、xIsNextの真偽値を反転させます。
この変更により、“X” 側と “O” 側が交互に着手できるようになります。また、Boardコンポーネントのrenderメソッド内にある “status” テキストも変更して、どちらのプレーヤの手番なのかを表示するようにしましょう。
index.jsrender() { const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); return ( <div> <div className="status">{status}</div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); } }ゲーム勝者の判定
ゲームが決着して次の手番がなくなった時にどちらが勝利したかを表示しましょう。また、既存のコードではすでに値が入ったSquareを再度クリックすると、値が反転してしまうので、これらの課題を解消していきます。
まずは、ファイル末尾に以下のヘルパー関数をコピーして貼り付けてください。
index.jsfunction calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }このヘルパーは、三目並べの勝利パターンである「タテ3・ヨコ3・ナナメ2」の合計8パターンに対して、同じ値が置かれているかを確認する役目を果たしています。もし勝者がいればその値を返し、いなければnullを返します。
このcalculateWinner(squares)ヘルパーを用いて、Boardコンポーネントのrenderメソッドを書き換えましょう。
index.jsrender() { const winner = calculateWinner(this.state.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } return ( // the rest has not changedここではまず、winnerに現状のゲームの結果を代入します。
winnerに値が入っていれば'Winner: 勝者'という文字列をstatusに代入し、winnerの値がnullであれば'Next player: 'X'or'O''をstatusに代入します。続いて、先ほど述べた「すでに値が入ったSquareを再度クリックすると値が反転してしまう」問題を解消します。
hundleClickメソッドを以下のように書き換えましょう。index.jshandleClick(i) { const squares = this.state.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); }calculateWinner(squares)かsquares[i]のいずれかに値が入っている場合にはreturnします。
こうすることはif文以下の処理が実行される前にメソッドを終了することができるので、値が書き換えられるのを防ぐことができます。盤面上部のテキストが変化し、埋まっているSquare(もしくは勝敗決定後)にクリックイベントが発生しなければOKです。
タイムトラベル機能の追加
以前の着手まで「時間を巻き戻す」ことができるようにしましょう。
そのためには、「着手の履歴」を保存しなければなりません、
ここで思い出して欲しいのですが、handleClickメソッドでは、常に元の配列のコピーを作成していました。handleClick(i) { const squares = this.state.squares.slice();このため、squares の過去のバージョンをすべて保存しておいて、過去の手番をさかのぼることができるようになります。
過去の squares の配列を、history という名前の別の配列に保存しましょう。
この history 配列は初手から最後までの盤面の全ての状態を記憶しており、以下のような構造を持っています。history = [ // Before first move { squares: [ null, null, null, null, null, null, null, null, null, ] }, // After first move { squares: [ null, null, null, null, 'X', null, null, null, null, ] }, // After second move { squares: [ null, null, null, null, 'X', null, null, null, 'O', ] }, // ... ]State のリフトアップ、再び
上に示したhistory配列は、Gameコンポーネントで管理することにします。
早速、Game コンポーネントのstateにhistoryを設置しましょうこうすることで、子であるBoardコンポーネントからsquaresのstateを、取り除くことができます。
Squareコンポーネントにあった「stateをリフトアップ」して Boardコンポーネントに移動したときと全く同様にして、今度は BoardにあるstateをトップレベルのGameコンポーネントにリフトアップしましょう。これにより GameコンポーネントはBoardのデータを完全に制御することになり、history内の過去の手番のデータをBoardに表示させることができるようになります。まず、Gameコンポーネントの初期stateをコンストラクタ内でセットします。
index.jsclass Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, }; } render() { return ( <div className="game"> <div className="game-board"> <Board /> </div> <div className="game-info"> <div>{/* status */}</div> <ol>{/* TODO */}</ol> </div> </div> ); } }次に、Boardコンポーネントが squaresとonClickプロパティを Gameコンポーネントからpropsで受け取るようにします。
renderSquareメソッド内の{this.state.squares[i]}と{this.handleClick(i)}を以下のように書き換え、Boardコンポーネントのコンストラクタは削除しましょう。index.jsclass Board extends React.Component { handleClick(i) { const squares = this.state.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ squares: squares, xIsNext: !this.state.xIsNext, }); } renderSquare(i) { return ( <Square value={this.props.squares[i]} ⬅︎ココ onClick={() => this.props.onClick(i)} ⬅︎ココ /> ); }Gameコンポーネントの内容を更新して、ゲームのステータステキストの決定や表示の際に最新の履歴が使われるようにします。
またGameコンポーネント側から、Boardに送るpropsの内容を設定します。以下にGameコンポーネントに加える変更をまとめて記載します。順を追ってその変更の意味を解説します。まずは、BoardコンポーネントのhandleClickメソッドを、Gameコンポーネントに移行します。
index.jsclass Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], xIsNext: true, }; } ⬇︎BoardのhandleClickを移行 handleClick(i) { ① const history = this.state.history; ② const current = history[history.length - 1]; ③ const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]){ return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ ④ history: history.concat([{ * squares: squares, * }]), xIsNext: !this.state.xIsNext, }); }①:定数historyにstateのhistoryを代入
②:定数currentにhistory配列のうち最新のもの(index番号が最大のもの)を代入
③:定数squaresに、currentに含まれている盤面の状態をコピーして代入※lengthは要素の数を返すので、起点は1です。対して配列の要素に割り当てられたindex番号は0を起点とするので、その差分を埋めるためにhistory.length 「-1」としています。
④:history配列に、変更が加わった新たな配列を生成し、追加。
続いて、renderメソッドの変更をしていきましょう。
index.js⬇︎renderの内容を変更 render() { ① const history = this.state.history; ② const current = history[history.length - 1]; ③ const winner = calculateWinner(current.squares); ④ const moves = history.map((step, move) => { * const desc = move? * 'Go to move #' + move : * 'Go to game start'; * return( * <li> * <button onClick={() => this.jumpTo(move)}>{desc}</button> * </li> * ); * }); let status; if (winner){ status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } return ( <div className="game"> <div className="game-board"> ⑤ <Board * squares={current.squares} * onClick={(i) => this.handleClick(i)} * /> </div> <div className="game-info"> <div>{status}</div> ⑥ <ol>{moves}</ol> </div> </div> ); } }①・②:先ほどと同様。
③:定数winnerに、最新の盤面の勝敗状況を代入。
④:定数movesにmap関数を用いて生成されたボタンのリストを代入。
⑤:Boardに渡すpropsを設定。
⑥:④で作ったリストを表示④のプロセスは私がやや混乱したポイントなので、丁寧に解説します。
見たらわかるという方は飛ばしてくださって構いません。まずmap関数は、「指定された配列を参照して、特定の処理を施した新たな配列を作り出すメソッド」である
ということを念頭に、このコードを読み解いていきましょう。ここでmapに引数として渡されているのは関数です。
その関数も引数を取っているのですが、
第一引数に元配列の要素を一つずつ呼び出し、
第二引数にその要素のインデックス番号が呼び出され、
要素一つづつに処理を加えたのちに、新たな配列が生成されます。つまり、
stepにはhistory[0]からhistory[8]までのオブジェクトが、
moveには0から8までのindex番号が渡されています。参考:https://teratail.com/questions/192578
const moves = history.map((step, move) => { * const desc = move? * 'Go to move #' + move : * 'Go to game start'; * return( * <li> * <button onClick={() => this.jumpTo(move)}>{desc}</button> * </li> * ); * });ここまで理解すれば、このコードを読むことに抵抗は感じないはずです。
ゲームの履歴内にある三目並べのそれぞれの着手に対応して、ボタンを有するリストアイテムを作りました。
ボタンにはonClickハンドラがありthis.jumpTo()というメソッドを呼び出していますが、このメソッドは後ほど実装します。Gameコンポーネントがゲームのステータステキストを表示するようになったので、対応するコードはBoard内のrenderメソッドから削除しましょう。Boardのrender関数は以下のようになります。
index.js(Boadコンポーネント)render() { return ( <div> <div className="board-row"> {this.renderSquare(0)} {this.renderSquare(1)} {this.renderSquare(2)} </div> <div className="board-row"> {this.renderSquare(3)} {this.renderSquare(4)} {this.renderSquare(5)} </div> <div className="board-row"> {this.renderSquare(6)} {this.renderSquare(7)} {this.renderSquare(8)} </div> </div> ); }
ボタンリストができればOKです。
また、jumpToメソッドが未定義なので、これらのボタンをクリックするとエラーが発生します。後ほどjumpToメソッドを定義します。タイムトラベルの実装
着手に応じたボタンリストを先ほど作成しましたが、実はまだ不十分です。
それぞれのリストアイテムをReactが別個のものとして捉えるためには、識別子が必要になるからです。
人間から見れば、ボタンテキストに含まれる数字を見てリストのうちのどのアイテムなのか判断することができますが、Reactにはできません。例えば、道端にいるスズメを個体毎に見分けるのは至難の技ですが、
別々の色がついたタグが足に括られていたとしたら、おそらく見分けられるでしょう。
Reactにも、このタグを用意する必要があります。
タグはReactにおいてkeyと呼ばれます。三目並べゲームの履歴内においては、すべての着手にはそれに関連付けられた一意なIDが存在します。
着手はゲームの最中に並び変わったり削除されたり挿入されたりすることはありませんから、
着手のインデックスをkeyとして付与しましょう。index.jsconst moves = history.map((step, move) => { const desc = move ? 'Go to move #' + move : 'Go to game start'; return ( <li key={move}> <button onClick={() => this.jumpTo(move)}>{desc}</button> </li> ); });まず、Gameコンポーネントのコンストラクタで、stateにstepNumber: 0を加えます。
これは、いま何手目の状態を見ているのかを表すのに使います。index.jsclass Game extends React.Component { constructor(props) { super(props); this.state = { history: [{ squares: Array(9).fill(null), }], stepNumber: 0, xIsNext: true, }; }次に、GameコンポーネントにjumpToメソッドを定義して、stepNumberが更新されるようにします。
jumpToメソッドの引数には、リストボタンを作成した際のmove(つまり、history内の要素のindex番号)が置かれています。また、更新しようとしているstepNumberの値が偶数だった場合はxIsNextをtrue に設定します。
index.jshandleClick(i) { // this method has not changed } jumpTo(step) { this.setState({ stepNumber: step, xIsNext: (step % 2) === 0, }); } render() { // this method has not changed }次に、Gameコンポーネントのrenderを書き換えて、常に最後の着手後の状態をレンダーするのではなく stepNumber によって現在選択されている着手をレンダーするようにします。
index.jsrender() { const history = this.state.history; const current = history[this.state.stepNumber]; const winner = calculateWinner(current.squares); // the rest has not changed最後に、マス目をクリックしたときに実行されるGameのhandleClickメソッドに、いくつか変更を加えます。
時間を巻き戻した時点でマス目がクリックされた場合にhistoryの配列 と stepNumberを更新する記述をします。
index.jshandleClick(i) { * const history = this.state.history.slice(0, this.state.stepNumber + 1); const current = history[history.length - 1]; const squares = current.squares.slice(); if (calculateWinner(squares) || squares[i]) { return; } squares[i] = this.state.xIsNext ? 'X' : 'O'; this.setState({ history: history.concat([{ squares: squares }]), * stepNumber: history.length, xIsNext: !this.state.xIsNext, }); }slice(a, b)は、指定した配列のindex番号が、a以上、b未満の要素を持つ配列としてコピーします。
4手目まで進んだ局面があったとします。
0・1・2・3・4*
2手目まで戻りましょう。
0・1・2*・3・4
jumpToメソッドでは、history内の要素を検索して、該当する盤面をレンダリングするだけです。
この状態(stepNumberが2の状態)で新たにマス目がクリックされると、handleClickが動作し、const history = this.state.history.slice(0, this.state.stepNumber + 1);の部分によって、0以上3未満の要素を持つ配列のコピーが作成されます。
0・1・2*・3・4
⬇︎ slice(0, this.state.stepNumber + 1)
0・1・2*こうすることで、現在の手'以降'のデータを持たない新たな配列でゲームをリスタートすることができます。
以前と同じように処理が流れ、新たな一手を追加した盤面のデータがhistoryに加えられます。
setStateのstepNumberに配列の長さを渡し、値を更新しましょう。
このように、ゲームの再スタートができるようになればOKです。完成!!
以上でチュートリアルは終了です。お付き合いくださりありがとうございました。
後書き
1人で学習を進めていたら、わかったフリしてただろうなぁとか、前後のニュアンスでなんとなく使っていた関数も改めて調べると知らない点が多くて、普段いかに”なんとなく”で作業しているのかが自覚できました。
何か間違っている点があったら教えてくださると幸いです。
- 投稿日:2020-05-23T12:16:51+09:00
ReactのuseRefを使う上での注意点
はじめに
DOMのバウンディングボックスなどを取得する場合、
hooks
のuseRef
を使いたいケースがあります。しかし、下位のコンポーネントにそれらの情報を渡す場合などは、注意が必要になります。例
次のようにメッセージの右下にいいねボタンを表示したいとします。
うまくいかないパターン
ThumbsUp
コンポーネントに対してボタン位置の基準となるHTMLElement
を渡し、ThumbsUp
にてボタンの表示位置を求めています。App.tsximport React, { useRef } from "react"; import ThumbsUp from "./ThumbsUp"; import "./styles.scss"; export default function App() { const ref = useRef<HTMLSpanElement | null>(); return ( <div> <span ref={ref} className="baloon"> こんにちは </span> {ref.current && ( <ThumbsUp anchor={ref.current} /> )} </div> ); }ThumbsUp.tsximport React, { useRef, useEffect } from "react"; import "./styles.scss"; type Props = { anchor: HTMLElement; }; export default function Portal({ anchor }: Props) { const ref = useRef<HTMLDivElement | null>(null); useEffect(() => { if (ref.current) { const anchorRect = anchor.getBoundingClientRect(); const position = { top: anchorRect.top + anchorRect.height - 20, left: anchorRect.left + anchorRect.width - 10 }; ref.current.style.top = `${position.top}px`; ref.current.style.left = `${position.left}px`; } }, [anchor]); return ( <div ref={ref} className="thumbs-up"> <button> <span role="img" aria-label="thumbs-up"> ? </span> </button> </div> ); }何が起こるか
残念ながら、意図通りにいいねボタンが表示されません。
これは公式にも記載されていますが、App.tsx
において、ref.current
が置き換わっても再レンダーは発生しないためです。そのため、ThumbsUp
はレンダリングされないままの状態となっています。解決策
コールバック式の
ref
とuseState
を使うことで解決することができます。下のようにspan
のref
に関数を与えることで、setAnchor
が呼ばれたタイミングで再レンダーを発生させ、意図通りThumbsUp
をレンダリングできるようになります。App.tsximport React, { useState } from "react"; import ThumbsUp from "./ThumbsUp"; import "./styles.scss"; export default function App() { const [anchor, setAnchor] = useState<HTMLSpanElement>(); return ( <div> <span ref={elm => { // コールバック関数を与える setAnchor(elm); }} className="baloon" > こんにちは </span> {anchor && <ThumbsUp anchor={anchor} /> } </div> ); }