- 投稿日:2019-07-14T21:44:00+09:00
GatsbyでNetlifyのFormを使う(react-bootstrap使用)
Gatsbyで作ったサイト(react-bootstrap使用)でNetlifyのFormを使おうとして微妙にハマったのでメモしておく。
react-bootstrapを使わない場合も参考になると思う。環境
- React 16.8.6
- Gatsby 2.1
- react-bootstrap 1.0.0-beta.9
コード
以下のコードで動作
class ContactForm extends Component { constructor() { this.state = { validated: false } } // react-bootstrap用のPOSTイベントハンドラ // https://react-bootstrap.github.io/components/forms/#forms-validation handleSubmit(event) { if (!event.currentTarget.checkValidity()) { event.preventDefault() event.stopPropagation() } this.setState({ validated: true }) } render (){ return ( <Form validated={this.state.validated} onSubmit={this.handleSubmit} name="contact" method="POST" data-netlify="true"> <input type="hidden" name="form-name" value="contact" /> <Form.Group> <Form.Label>name</Form.Label> <Form.Control name="name" type="text" /> </Form.Group> <Button type="submit" >送信</Button> </Form> ) } }ポイント
まずは、netlifyの公式通りにFormタグに
name="contact"
及びdata-netlify="true"
プロパティを追加する。ちゃんとreact-bootstrapでも動作した。次に、Gatsbyの場合、Netlify側が挿入する以下のタグがGatsbyによって消去されるため、予め入力しておく必要がある。ちなみに、この設定方法は公式ブログに書いてある。
<input type="hidden" name="form-name" value="contact" />また、
<Form.Control />
(普通のhtmlの場合は<input />
)にはname
プロパティが必要。name
とvalue
プロパティの値が、ユーザーへ通知される仕組み。
- 投稿日:2019-07-14T21:33:46+09:00
ReactアプリをFirebaseにのっける最も簡単な方法
ReactとFirebaseの組み合わせで使用できる、ちょっとしたマイナーツールを紹介。
generator-react-firebase - npm
Yeoman generator for starting projects using React and Firebase (Redux optional)
Yeoman
とは聞きなれない単語がありますが、ただの形容詞ではなく、yeoman.ioのことです。
辞書で引いてみると、1《主に英国で用いられる》 a《あまり用いられない語》 自作農,小地主. b(昔の)自由民,ヨーマン 《独立自営農民; gentleman より低い地位の自由所有権保有者 (freeholder)》. c(昔,王家・貴族に仕えた高位の)従者. 2【米海軍】 事務係下士官.よしなにいい感じに気を利かして作業をやってくれますよ、といったところでしょうか。
本家サイトによれば、scaffolding tool
,build tool
,package manager
を含むエコシステムをいい感じに提供してくれるらしいです。イケてそうですね。この中で、
scaffolding tool
とかいうのが気になるやつです。
これはgeneratorを作るgeneratorであり、generator-react-firebase
はyeoman
でscaffoldされています。
yeoman
を使って作られたgeneratorは、他にもたくさんあります。vscode-generator
hyperledger-composer
などなど。それ以外のgenerator: https://yeoman.io/generators/
generator-react-firebase
とはweb-app-generatorの一つであり、react-redux-firebaseを使用してアプリをscaffoldします。
本来であれば、フロントエンドのイケイケ開発を始めるには、深く広い(webpackやgulp他様々の)知識が必要になります。
generatorを使えばそこをさっと飛び越えてしまえるのが、イケイケポイントですね。
generator-react-firebase
の使い方Readmeに書かれている通り、
yo
を実行して、あとは質問に答えていくだけです。npm install -g yo generator-react-firebase yo react-firebase予め
firebase login
とかしておけば、イイカンジにレコメンドしてくれるので、とても簡単にreactアプリをscaffoldできます。注意点
コンソールで打ち込む情報の中に、FirebaseプロジェクトのApiKeyが含まれます。
これはシークレットなので隠すべき(公開レポジトリにpushしてはいけない)ですが、.gitignore
に登録されていないファイル.yo-rc.json
に紛れ込みます。お気をつけて。
- 投稿日:2019-07-14T18:16:14+09:00
ReactでHTMLを作成し、Clickした際にそれぞれの情報をモーダルに表示・非表示させる(Railsで)
はじめに
ReactでHTMLを作成するまでは案外すんなりと行ったのですが、クリックした際に「それぞれの情報を持たせてモーダルを表示する」ことに苦労したので忘備録としてまとめておきます。
最終的にはこのような感じになります。
たくさんの方法を試したので不必要なものもあるかもしれません。
その点は指摘していただけると幸いです。
下の画像をクリックした際にその情報を表示させています。
表示させる内容は個人で変更してください。
1.gemの導入とインストール
この2つのgemをGemfileに加え、
bundle install
してくださいGemfilegem 'react-rails' gem 'webpacker'その後、それぞれインストールしてください
$ bundle install $ rails webpacker:install $ rails webpacker:install:react $ rails generate react:installするとapp以下に
app/javascript/components
フォルダが作成されます。
私自身、app/assets
以下にJavascriptフォルダがあるのに大丈夫なのかと思いましたが、問題ありません。2. application.html.hamlにtagを追加
application.html.hamlに以下の記述を加えてください
application.html.haml= javascript_pack_tag 'application'この際にTerminalでyarnがどうこうというエラーが出るかもしれません。
その場合$ yarn install
等、各自で調べて解決してください。これらでRails上でReactを使う準備ができましたので、いよいよjsファイルに記述していきます。
3. Components以下にjsファイルの作成
Components以下に
App.js
とGraduate.js
を作成します。
これらの名前は自分で決めてくださって結構です。
ではこちらのコードをコピーしてくださいApp.jsimport React from 'react'; import Graduate from './Graduate'; const lessonList = [ { name: '開成太郎(2017)', school: '一条高校', image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg', introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました', }, { name: '開成花子(2016)', school: '奈良高校', image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg', introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました', }, { name: '開成三郎(2019)', school: '郡山高校', image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg', introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。', }, { name: '開成四郎(2015)', school: '登美ケ丘高校', image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg', introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。', } ]; class App extends React.Component { render() { return ( <div className="performance"> {lessonList.map((lessonItem) => { return ( <Graduate name={lessonItem.name} image={lessonItem.image} school={lessonItem.school} introduction={lessonItem.introduction} /> ); })} </div> ); } } export default App;それでは解説していきます。
import React from 'react'; import Graduate from './Graduate';まずimportとは輸入という意味です。
その名の通り、一行目ではReactを二行目ではGraduateファイルを読み込んでいます。
このおかげでReactを使うことができ、またGraduateにパラメーター(props)を渡すことができます。const lessonList = [ { name: '開成太郎(2017)', school: '一条高校', image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg', introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました', }, { name: '開成花子(2016)', school: '奈良高校', image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg', introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました', }, { name: '開成三郎(2019)', school: '郡山高校', image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg', introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。', }, { name: '開成四郎(2015)', school: '登美ケ丘高校', image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg', introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。', } ];この部分ではそれぞれのハッシュを配列に入れています。これをあとでmapメソッドで一つずづ表示させていきます。
class App extends React.Component { render() { return ( <div className="performance"> {lessonList.map((lessonItem) => { return ( <Graduate name={lessonItem.name} image={lessonItem.image} school={lessonItem.school} introduction={lessonItem.introduction} /> ); })} </div> ); } }この部分でComponentを作成しています。(extendsは広げるという意味)
ConmonentはJavascriptの関数のようなものです。
その中のreturnでHTMLを返し、表示させています。またこのような記法をJSX
と言います。
また、JSXには約束事があり、複数の要素を返すことができません。なので図の場合、performanceクラスを親要素としその中に色々と要素を追加しています。
{lessonList.map((lessonItem)
ではlessonListの数だけ繰り返し、returnを読み込んでいます。
return内でとすることでGraduateコンポーネントを呼び出しています。これが可能なのはimport Graduate from './Graduate';
のおかげです。
sosite
呼び出す際にはname,school,image,introductionのパラメーターを渡しています。(props)
このパラメーターをGraduate.js側で使うわけです。ちなみにJSX内でjavascriptの記法を用いる際は中括弧が必要なため、中括弧内に書いてあります。export default App;App.jsの最後の行ですが、exportとは輸出という意味です。
これのおかげでHTMLファイルでApp.jsを呼び出すことができ、returnとして要素をHTMLに追加することができます。App.jsがGraduateを二行目でimportできているのもGraduate.jsで最後にexport default Graduate;
としているからです。Graduate.jsimport React from 'react'; class Graduate extends React.Component { constructor(props) { super(props); this.state = {isModalOpen: false}; } handleClickLesson() { this.setState({isModalOpen: true}); } handleClickClose() { this.setState({isModalOpen: false}); } render() { let modal; if (this.state.isModalOpen) { modal = ( <div className='modal-area'> <div className='modal-inner'> <div className='modal-header'></div> <div className='modal-introduction'> <h2>{this.props.name}</h2> <p>{this.props.introduction}</p> </div> <button className='modal-close-btn' onClick={() => this.handleClickClose()} > とじる </button> </div> </div> ) }; return ( <div className="graduate"> <img className="graduate__image" src={this.props.image} onClick={() => {this.handleClickLesson()}} /> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div> ); } } export default Graduate;この部分で、実際に表示されるHTMLを作成しています。App.jsから渡されたパラメータ(props)を使って書いていきます。
constructor(props) { super(props); this.state = {isModalOpen: false}; }新しくstate(状態)というものが出てきましたが、propsとstateは少し異なり、stateはそのComponent内で保持されるものであって、propsみたいにComponentからComponentに渡すことはできません。詳しくはこちら
この部分でモーダルの表示・非表示を管理しています。handleClickLesson() { this.setState({isModalOpen: true}); } handleClickClose() { this.setState({isModalOpen: false}); }ここで先ほどのstateを関数(
handleClickLesson
orhandleClickClose
)が呼ばれた時に更新しています。この時に注意して欲しいのが、更新する際はsetState
としないといけないことです。let modal; if (this.state.isModalOpen) { modal = ( <div className='modal-area'> <div className='modal-inner'> <div className='modal-header'></div> <div className='modal-introduction'> <h2>{this.props.name}</h2> <p>{this.props.introduction}</p> </div> <button className='modal-close-btn' onClick={() => this.handleClickClose()} > とじる </button> </div> </div> ) };ここで
isModalOpen
がtrueの場合のみ、変数modalに値を代入し、表示させます。
そしてモーダルの中にonClick={() => this.handleClickClose()}
があると思いますが、このボタンを押すことでisModalOpen
がfalseになり再び非表示になります。
またApp.jsからもらってきたパラメータ(props)を{this.props.name}とすることで代入することができます。
またReactではclassをclassNameと記載します。return ( <div className="graduate"> <img className="graduate__image" src={this.props.image} onClick={() => {this.handleClickLesson()}} /> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div> );最後ですね。
この部分で常時表示させるHTMLをApp.jsからもらったpropsを使って作成しています。
そして{modal}
の部分で先ほどの変数を代入しているわけですね。imgクラスにonClickが設定されているため、画像をクリックするとisModalOpen
がtrueになり、値が代入されたmodalが表示されるわけです。
では最後にHTML側でApp.jsを呼んであげましよう。hamlの場合は
index.haml.haml= react_component("App")htmlの場合は
index.html.erb<%= react_component("App") %>これで完成です。
CSSだけ記載しておきます。
お好みで変更してください。stylesheet.css.performance { .graduate { padding: 30px 0; display: inline-block; width: 25%; text-align: center; &__student { font-size: 15px; padding-top: 10px; text-align: right; padding-right: 20px; } &__school { font-size: 20px; padding-top: 10px; } &__image { cursor: pointer; height: 160px; width: 160px; border-radius: 50%; } .modal-area { z-index: 2; position: fixed; top: 0; right: 0; bottom: 0; left: 0; background-color: rgba(0, 0, 0, 0.6); .modal-inner { position: absolute; top: 8%; right: 0; left: 0; width: 480px; padding-bottom: 60px; margin: auto; background-color: rgb(255, 255, 255); .modal-header { margin-bottom: 60px; } .modal-introduction p { color: #5876a3; width: 384px; line-height: 32px; text-align: left; margin: 36px auto 40px; } .modal-close-btn { font-size: 13px; color: #8491a5; width: 200px; padding: 16px 0; border: 0; background-color: #f0f4f9; cursor: pointer; } .modal-close-btn:hover { color: #8491a5; background-color: #ccd9ea; transition: .3s ease-in-out; } } } } }番外編
Click時にモーダルが表示されるが非表示にならない場合
モーダル実装時にモーダルが閉じないというバグが起こりました。
原因を調べてみると閉じるボタンを押した際に一回閉じてから再度開いています。
これがその時のコードです。<div className="graduate" onClick={() => {this.handleClickLesson()}}> <img className="graduate__image" src={this.props.image}/> <div className="graduate__school">{this.props.school} 合格!</div> <div className="graduate__student">{this.props.name}</div> {modal} </div>何が問題かというと一番の親要素であるgraduateにopenmodalを設定しているせいで、その子要素であるモーダル内のclossmodalを押した際に同時に親要素のopenmodalも呼ばれてしまうからです。
なのでそれぞれのClick機能は親要素、子要素の関係に注意しましょう。参考資料
React公式HP
Progate
https://qiita.com/k-penguin-sato/items/e3cc04f787cf3254cfae
https://qiita.com/kyrieleison/items/78b3295ff3f37969ab50
- 投稿日:2019-07-14T16:56:24+09:00
Reactアプリの枠組みの雛形を作ってみる
タイトルが「Reactアプリの枠組みの雛形を作ってみる」とありますが、本記事の目的は、以下のような複数のReactライブラリを一緒に使っても、それぞれのライブラリの機能がそこなわれることなく機能することを確認することです。
個人的には、antdが豊富なUIコンポネントを提供してくれているので、今後Material-UIに替えて使っていければなーと思っています。(中国発ですが、トランプ制裁とか関係ないですよね)immutable jsがリストから落ちていますが、今後の課題です。
- @loadable/component
- redux
- react-redux
- redux-logger
- redux-thunk
- react-router-dom
- connected-react-router
- antd
- styled-components
@loadable/component
Reactのコード分割を行うライブラリ。バンドルの肥大化対応。React code splitting made easy.redux
A predictable state container for JavaScript apps.react-redux
Official React bindings for Redux
「React Reduxの概要を理解する」redux-logger
Logger for Redux。 reduxのstateログを出力するMiddleware。redux-thunk
Thunk middleware for Redux.
actionとして非同期関数を指定することが可能になります。react-router-dom
react-routerconnected-react-router
A Redux binding for React Router v4 and v5
history methods (push, replace, go, goBack, goForward)のdispatch が、 redux-thunk と redux-sagaの両方に互換性を持ちます。
「react-router v4 と Redux」antd
Ant Design of React
ReactのUIライブラリ。豊富なUIコンポネントを比較的簡単に使える。
「React UI library の antd について (1) - Button」styled-components
Visual primitives for the component age
JSでstyleを記述するCSS in JSのライブラリ。【補足】
実は本検証の過程で、redux-formをreact-router-domと一緒に使うと機能しないことが確認できました。reduxForm()とconnect()という2つのHOCで2重にラップした時に、propsが、最終的なコンポーネントにうまく伝わっていかない感じでした。結論としてはredux-formは使わずに、antdのForm.create()を使ってvalidateを行うようにしました。私の検証ミスかもしれませんが、丸一日試行錯誤した結果です。1.インストール
以下のコマンドで環境構築OKです。
yarn create react-app antd-test cd antd-test yarn add @loadable/component yarn add redux react-redux redux-logger redux-thunk yarn add react-router-dom connected-react-router yarn add antd styled-components一応package.jsonも掲載しておきます。
package.json{ "name": "antd-test", "version": "0.1.0", "private": true, "dependencies": { "@loadable/component": "^5.10.1", "antd": "^3.20.2", "connected-react-router": "^6.5.2", "react": "^16.8.6", "react-dom": "^16.8.6", "react-redux": "^7.1.0", "react-router-dom": "^5.0.1", "react-scripts": "3.0.1", "redux": "^4.0.4", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", "styled-components": "^4.3.2" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }2.画面イメージ
本アプリのページは、ホーム画面とユーザ登録画面、ログイン画面の3つです。ページ遷移も含めて以下に紹介します。
ユーザ登録が成功すると、3秒後にログイン画面に自動遷移します。
ログインが成功すると、3秒後にホーム画面に自動遷移します。
3.ソースツリー
以下がソースファイルのツリーになります。
containersディレクトリが重要です。その中でもLoginとRegisterがメインです。Loadable.jsがimportポイントとなります。index.jsがreact-reduxのcontainerであり、Register.jsとLogin.jsがcomponentとなります。index.jsがReduxの処理を行い、Register.jsとLogin.jsはReduxについては何も知らないことになっています。$ tree src src ├── App.js ├── actions │ └── actions.js ├── components │ ├── Footer.js │ ├── Header.js │ └── Wrapper.js ├── containers │ ├── Home │ │ ├── Loadable.js │ │ └── index.js │ ├── Login │ │ ├── Loadable.js │ │ ├── Login.js │ │ └── index.js │ ├── NotFoundPage │ │ ├── Loadable.js │ │ └── index.js │ ├── Register │ │ ├── Loadable.js │ │ ├── Register.js │ │ ├── Register.org2 │ │ ├── _Register.new │ │ ├── _Register.org │ │ └── index.js │ └── input.js ├── createStore.js ├── form-style.css ├── index.js └── reducers └── index.js4.index.js
index.jsでReduxとreact-redux、connected-react-routerの初期設定を行います。
src/index.jsimport React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import { ConnectedRouter } from 'connected-react-router'; import createBrowserHistory from 'history/createBrowserHistory'; import App from './App' import createStore from './createStore' // connected-react-router - action経由でルーティングが可能、push,replace.. // historyを強化 const history = createBrowserHistory(); const store = createStore(history); const dest = document.getElementById('root') let render = () => { ReactDOM.hydrate( <Provider store={store}> <ConnectedRouter history={history}> <App /> </ConnectedRouter> </Provider>, dest ) } render()createStore.jsはreduxのオリジナルcreateStore.jsのラッパーです。ReducersとMiddlewareの設定を行います。
src/createStore.jsimport { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux' import logger from 'redux-logger' import thunk from 'redux-thunk' import { routerMiddleware, connectRouter } from 'connected-react-router' import * as reducers from './reducers' // connected-react-router - action経由でルーティングが可能、push,replace.. // createStoreの再定義 - historyを引数で受け、connected-react-routerの利用を抽象化 export default function createStore(history) { return reduxCreateStore( // オリジナル createStore の別名 combineReducers({ ...reducers, router: connectRouter(history) }), applyMiddleware( logger, thunk, routerMiddleware(history) ) ); }reducersの定義です。
Redux storeは次の2つのstateを保持します。
- state.users[]: userオブジェクトの配列
- state.logined: ログインしているuserオブジェクト
src/reducers/index.jsexport const users = (state = [], action) => { switch (action.type) { case 'ADD_USER': // *** userを追加 return [ ...state, // *** 分割代入、stateに追加 { email: action.user.email, name: action.user.name, password: action.user.password } ] default: return state } } export const logined = (state = {}, action) => { switch (action.type) { case 'ADD_LOGINED_USER': // *** userを追加 return ( { email: action.user.email, name: action.user.name, password: action.user.password }) default: return state } }redux-thunkを使っているので、actionは非同期関数で定義しています。ただし非同期はsetTimeout()で模擬したものです
connected-react-routerを使っており、提供されるpush()はredux-thunkと互換性があります。push()を使って、ユーザ登録成功後にログイン画面へ、ログイン成功後にホーム画面へ、自動リダイレクトしています。src/actions/actions.jsimport { push } from 'connected-react-router'; const addUser = user => ({ type: 'ADD_USER', user: user }) const addLoginedUser = user => ({ type: 'ADD_LOGINED_USER', user: user }) export const asyncAddUser = values => { return (dispatch, getState) => { setTimeout( () => { dispatch(addUser(values)) dispatch(push("/login")) }, 3000 ); } } export const asyncLogin = values => { return (dispatch, getState) => { setTimeout( () => { const state = getState() for (const user of state.users) { if( user.email === values.email && user.password === values.password ) { console.log("login succeful!!!",values) dispatch(addLoginedUser(user)) dispatch(push("/")) return } } console.log("login failed!!!",values) }, 3000 ); } }5.App.js
App.jsはこのアプリのメインになります。サイト全体のページ構成を定義します。
- react-routerでRouteを定義します。
- 画面の枠組みを定義して、全体のstyleを実装します
styleはstyled-componentsを利用しているほか、antd.cssとform-style.cssを読み込んでいます。
App.jsimport React, { Component } from 'react' import styled from 'styled-components' import { Switch, Route } from 'react-router-dom' import Home from './containers/Home/Loadable' import LoginPage from './containers/Login/Loadable' import RegisterPage from './containers/Register/Loadable' import NotFoundPage from './containers/NotFoundPage/Loadable' import Header from './components/Header' import Footer from './components/Footer' import 'antd/dist/antd.css'; import './form-style.css'; const AppWrapper = styled.div` max-width: calc(768px + 16px * 2); margin: 0 auto; display: flex; min-height: 100%; padding: 0 16px; flex-direction: column; background: papayawhip; .btn { line-height: 0; } `; class App extends Component { render() { return ( <AppWrapper> <Header /> <Switch> <Route exact path="/" component={Home} /> <Route exact path="/login" component={LoginPage} /> <Route exact path="/register" component={RegisterPage} /> <Route path="" component={NotFoundPage} /> </Switch> <Footer /> </AppWrapper> ); } } export default App;form-style.cssはForm画面のstyle記述しています。ユーザ登録画面とログイン画面だけで読み込めばいいのですが、面倒なのでApp.js一か所で読み込んでいます。
src/form-style.css.form-register-containers { width: 100%; margin: auto; max-width: 400px; padding: 50px 10px; } .form-register-containers .center { text-align: center; } .form-register-containers .ant-form-item-label { line-height: 1; } .form-register-containers .ant-form-item-with-help { margin-bottom: 0; }6.ユーザ登録
loadableを通して、containerコンポーネントのindex.jsを読み込みます。
src/containers/Register/Loadable.jsimport loadable from '@loadable/component'; export default loadable(() => import('./index'));index.jsはcontainerコンポーネントとして、react-reduxの設定を行い、connect()でcomponentをラップします。加えてantdのForm.create()でラップしています。
2つのHOCを使っているのですが、順番に注意してください。src/containers/Register/index.jsimport { connect } from 'react-redux' import { asyncAddUser } from '../../actions/actions' import Register from './Register'; import { Form } from 'antd'; function mapStateToProps(state) { return state } function mapDispatchToProps(dispatch) { return { onSubmit : values => { dispatch(asyncAddUser(values)) } } } let myRegister = connect(mapStateToProps, mapDispatchToProps)(Register) myRegister = Form.create({ name: 'register_form' })(myRegister); export default myRegisterantdのFormを使って、ユーザ登録のform画面を定義しています。Form.create()でラッピングしているので、様々なvalidate関数を利用できます。
src/import React from 'react'; import { Form, Input, Icon, Button } from 'antd'; class Register extends React.Component { state = { confirmDirty: false, autoCompleteResult: [], }; componentDidMount() { // To disabled submit button at the beginning. this.props.form.validateFields(); } handleSubmit = e => { console.log(this.props) e.preventDefault(); this.props.form.validateFields((err, values) => { if (!err) { console.log('Submit OK: ', values); this.props.onSubmit( values ) } else { console.log('Submit NG: ', values); } }) } handleConfirmBlur = e => { const { value } = e.target; this.setState({ confirmDirty: this.state.confirmDirty || !!value }); }; compareToFirstPassword = (rule, value, callback) => { const { form } = this.props; if (value && value !== form.getFieldValue('password')) { callback('Two passwords that you enter is inconsistent!'); } else { callback(); } }; validateToNextPassword = (rule, value, callback) => { const { form } = this.props; if (value && this.state.confirmDirty) { form.validateFields(['confirm'], { force: true }); } callback(); }; render() { const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form; // Only show error after a field is touched. const emailError = isFieldTouched('email') && getFieldError('email'); const nameError = isFieldTouched('name') && getFieldError('name'); const passwordError = isFieldTouched('password') && getFieldError('password'); const confirmError = isFieldTouched('confirm') && getFieldError('confirm'); const buttonDisable = getFieldError('email') || getFieldError('name') || getFieldError('password') || getFieldError('confirm') return( <Form onSubmit={this.handleSubmit} className="form-register-containers"> <h1 className="center"> ユーザ登録 </h1> <Form.Item label="メールアドレス" validateStatus={emailError ? 'error' : ''} help={emailError || ''}> {getFieldDecorator('email', { rules: [ {type: 'email', message: 'The input is not valid E-mail!',}, { required: true, message: 'Please input your email!' }], })( <Input prefix={<Icon type="mail" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="email" />, )} </Form.Item> <Form.Item label="パスワード" validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}> {getFieldDecorator('password', { rules: [ { required: true, message: 'Please input your password!' }, { validator: this.validateToNextPassword,} ], })( <Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="password" />, )} </Form.Item> <Form.Item label="確認パスワード" validateStatus={confirmError ? 'error' : ''} help={confirmError || ''}> {getFieldDecorator('confirm', { rules: [ { required: true, message: 'Please input your confirmPassword!' }, { validator: this.compareToFirstPassword,} ], })( <Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="confirmPassword" onBlur={this.handleConfirmBlur} />, )} </Form.Item> <Form.Item label="名前" validateStatus={nameError ? 'error' : ''} help={nameError || ''}> {getFieldDecorator('name', { rules: [{ required: true, message: 'Please input your name!' }], })( <Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="name" />, )} </Form.Item> <Form.Item className="center"> <Button type="primary" htmlType="submit" className="btn-submit" disabled = {buttonDisable} > ユーザ登録 </Button> </Form.Item> </Form> ) } } export default Register7.ログイン
ログイン画面はユーザ登録画面と構成が全く同じです。説明も重複になるので、省略します。
src/containers/Login/Loadable.jsimport loadable from '@loadable/component'; export default loadable(() => import('./index'));src/containers/Login/index.jsimport { connect } from 'react-redux' import { asyncLogin } from '../../actions/actions' import Login from './Login'; import { Form } from 'antd'; function mapStateToProps(state) { return state } function mapDispatchToProps(dispatch) { return { onSubmit : values => { dispatch(asyncLogin(values)) } } } let myLogin = connect(mapStateToProps, mapDispatchToProps)(Login) myLogin = Form.create({ name: 'login_form' })(myLogin); export default myLoginsrc/containers/Login/Login.jsimport React from 'react'; import { Form, Input, Icon, Button } from 'antd'; class Login extends React.Component { componentDidMount() { // To disabled submit button at the beginning. this.props.form.validateFields(); } handleSubmit = e => { console.log(this.props) e.preventDefault(); this.props.form.validateFields((err, values) => { if (!err) { console.log('Received values of form: ', values); this.props.onSubmit( values ) } }) } render() { const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form; // Only show error after a field is touched. const emailError = isFieldTouched('email') && getFieldError('email'); const passwordError = isFieldTouched('password') && getFieldError('password'); const buttonDisable = getFieldError('email') || getFieldError('password') return( <Form onSubmit={this.handleSubmit} className="form-register-containers"> <h1 className="center"> ログイン </h1> <Form.Item label="メールアドレス" validateStatus={emailError ? 'error' : ''} help={emailError || ''}> {getFieldDecorator('email', { rules: [ {type: 'email', message: 'The input is not valid E-mail!',}, { required: true, message: 'Please input your email!' }], })( <Input prefix={<Icon type="mail" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="email" />, )} </Form.Item> <Form.Item label="パスワード" validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}> {getFieldDecorator('password', { rules: [{ required: true, message: 'Please input your password!' }], })( <Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="password" />, )} </Form.Item> <Form.Item className="center"> <Button type="primary" htmlType="submit" className="btn-submit" disabled = {buttonDisable} > ログイン </Button> </Form.Item> </Form> ) } } export default Login8.ホーム
ホーム画面は特にありません。
src/containers/Home/Loadable.jsimport loadable from "@loadable/component"; export default loadable(() => import("./index"));src/import React from 'react'; import { Route, Link } from 'react-router-dom'; const Home = () => ( <div> <h1>私のホームページへようこそ !!!</h1> <ul> <li><Link to="/login">Login</Link></li> <li><Link to="/register">Register</Link></li> </ul> </div> ); export default Home;URLで指定されたページが見つからい場合は、以下のcomponentが表示されます。
src/containers/NotFoundPage/Loadable.jsimport loadable from "@loadable/component"; export default loadable(() => import("./index"));src/containers/NotFoundPage/index.jsimport React from "react"; export default class NotFound extends React.PureComponent { render() { return <h1>This is the NotFoundPage Page!</h1>; } }9.ヘッダー/フッター
ヘッダーとフッターですが、特に説明は不要でしょう。
src/components/Header.jsimport React from 'react'; import { Link } from "react-router-dom"; import { Breadcrumb } from 'antd'; function Header() { return ( <Breadcrumb> <Breadcrumb.Item> <Link to="/">ホーム</Link> </Breadcrumb.Item> <Breadcrumb.Item> <Link to="/login">ログイン</Link> </Breadcrumb.Item> <Breadcrumb.Item> <Link to="/register">ユーザ登録</Link> </Breadcrumb.Item> </Breadcrumb> ); } export default Header;src/import React from 'react'; import Wrapper from './Wrapper'; function Footer() { return ( <Wrapper> <section>Footer footer Footer!!!</section> </Wrapper> ); } export default Footer;src/components/Wrapper.jsimport styled from 'styled-components'; const Wrapper = styled.footer` display: flex; justify-content: space-between; padding: 3em 0; border-top: 1px solid #666; `; export default Wrapper;今回は以上です。
- 投稿日:2019-07-14T15:11:16+09:00
【Firebase Auth】1つのアカウントにメールアドレスと電話番号を紐付ける
FirebaseのAuthenticationを利用したプロダクトの開発を行う機会があり、一つのアカウントにメールアドレスと電話番号を紐付ける必要があったのでその手順を残しておきます。
とても簡単に実装できました。参考: https://firebase.google.com/docs/auth/web/phone-auth?hl=ja
今回の手順をgitで公開しています。こちらから確認できます。
googleでログインさせた後に電話番号で認証させるといったことも簡単に実現できそうですね。
- 投稿日:2019-07-14T03:38:45+09:00
「SPAらしさ」について考える(メモ)
ここ最近Reactを使ったSPAの開発に勤しんでいたところ
やればやるほどSPAってなんなんだとなった。
単一ページ?いやでも複数ページあるけど。
ゲシュタルト崩壊を起こした。エスピーエー ワカラナイ ググル
ググッタ メイカク テイギ ナイSPAというのはなんとなく定義されているものの明確な定義はないらしい。
というわけでどういうものが「SPA」だと感じるのか
ひいては「SPAらしさ」とはなにかについて考察してみた。結論
あらかじめ結論を書いてしまうと
この記事を書いた結果以下の結論に行き着いた(あくまで個人の見解)SPAらしさ=ネイティブっぽいかどうか
以下はSPAらしさってなんだとか
なんかもういろいろごちゃごちゃ語っています。SSRはどうか
SSRってそもそもサーバーでHTML作ってるし
普通に通信発生してるしバックエンドとフロントエンドのソースコードを共通化できる
アイソモーフィックではあるものの
SPAらしくない。結局通信してURLに対し異なるHTMLを返すというのは
先祖返りというか、それSPAで作る意味あったの。。。という疑惑を抱いてしまうので「SPAらしくない」と判断。
(でも検索ロボットに対してのみSSRするならSPAらしいと思う)
SPAの利点?
通常のWebページではできないUXができる
高速なページ遷移
読み込みが早いなど、色々言われてはいるものの
正直そこまでメリットと言えるレベルか?という思いもあり
SPA自体にはさほどメリットはないと思っている。SPAは副産物的なものでは?
SPAがトレンドっぽくなっているのは
SPAにメリットがあるのではなくバックエンド側をシンプルなAPIの実装のみにすることにメリットがあった
と言う方が正しいと思っている。JSONを返すだけのAPIは
ネイティブからもウェブからも利用できるじゃぁ次はネイティブもフロントも同じコードで書けたらいいよね
とアイソモーフィックを目指した結果のSPAと考えた方がしっくりくる。
SPAは早い?
SPAは読み込みが早いと言われることもあるが
JSが肥大化して実はそんな早くないということも珍しくないと思うしパフォーマンスチューニングによって
最終的に従来のウェブサイトより早くできるとは思うもののそれがすごいメリットと言えるほどかというと微妙なラインでは?
jQueryでは限界がある?
よく複雑なDOM操作をjQueryでやるのは大変と説明されることもあるが
DOM操作だけで考えた場合、jQueryの方が優れていると感じる面もある。
従来のウェブサイトらしいサイトであればjQueryのがはるかに簡単だし
十分なケースも多いと思われる。ただDOM生成やネイティブアプリレベルのDOM操作
という観点で考えた場合はやはりReactの方が優れていると感じる。
(あとVDOMによる差分更新もメリットか)ネストされたDOM構造をjQueryで頑張って作っていくというのは
不可能ではないがあまり書きたいとは思えない。(好みの問題)Reactを使う理由
個人的になぜReactを使うかという事を改めて考えた場合
①バックエンドはAPIだけにしたい
②DOM生成をjQueryでやるのはつらい
③VDOMによる差分レンダリングだから無駄が少ないという3点からReactを採用しているようだ。
(なんとなく利点は感じていたものの、改めて自覚した)将来的にネイティブにも横展開できるかどうかという点はさほど考えていない。
(簡単にできるなら嬉しいなー程度)個人的な悩みの解決
従来のWebサイトのようなSPAっぽくないサイトを
Reactで作る意味はあるのかというもやもやがあったのだが今持てる知識の中で
バックエンドはAPIのみにしたいとなった場合フロントは以下の2択を迫られる
①静的なhtmlでサイトを作るか
②SPAで作るかSPAで作るメリットあるのかと思っていたが
バックエンドをAPIだけにできるじゃんと考えるとスッキリできた。主となるのがフロントなのかバックエンドなのか
これは個人的な感覚が強い部分だが
APIからデータを取得する頻度は少ない方がSPAらしく感じる。ゲームなんかでは、基本的にプレイ中は常にオンメモリでデータが管理され
セーブ時に必要なデータをまとめてHDD等に保存するこの作りに近いほどSPAらしさを感じる。
SPAらしくないと感じる例としては
何かしらデータを更新するたびに最新のデータをAPIから取得しなおして
画面を全更新かけるような作りである。基本的にフロント側でデータを管理し
バックエンドはデータの保管場所程度
というくらいがSPAらしいのではないかと感じる。まとめ
上記を踏まえたまとめ
SPAのメリット
①バックエンドをAPIだけにできる
②ネイティブの作りっぽくなるほど恩恵が得られやすいReactのメリット
①DOM生成楽
②ある程度規模が大きくなっても管理しやすい(実力次第でもある)SPAらしさ
ネイティブに近いほどSPAらしさが出る
SPAらしさを損なう要因
- SSR
- APIに頼りすぎるDOM更新
あとがき
五月雨に書きなぐった事で
今までもやもやしていた事が自分なりに整理されました。そしてSPAは難しいと言う事も再確認できました。
SPAを開発するとなった場合、多くはReactかVueを使うことになると思いますが
そもそもjQueryだけでもそこそこ複雑なアプリケーションを作る事は可能です。
にも関わらずReactやVueという新しいフレームワークが誕生したのは
jQueryで作るのが難しいくらい複雑なものを作ろうとしているからだと思いました。いくらReactを使えばSPAが作れるといったところで
そもそもSPAが難しいジャンルであり
(特にReactはあくまでViewライブラリ、Store管理は別途知識が必要ですし)それなりの技術力、設計力、経験値がなければ
ReactやVueを使ったところでカオスになってしまう。でも、だからこそ挑戦しがいがあり、面白く感じる自分がいる
そうも思いました。終わり。
- 投稿日:2019-07-14T00:17:15+09:00
mobx-react-lite入門 前編: mobx-react-liteのObserver
0.はじめに
JavaScriptの「シンプルかつスケーラブルな」状態管理ライブラリことMobXをReactと結びつけて、楽しくWebアプリケーションを作れるようになってみたいと思いませんか?
当記事ではReactとMobXを組み合わせて使うためのライブラリmobx-react-liteを使って、観測可能な状態と観測者による状態管理を俯瞰してみたいと思います。
前編では、mobx-react-liteで提供されるObserverを紹介します。
環境と想定読者
node.jsのインストールが必要です。node.jsを使うならば、備え付けのパッケージマネージャの
npm
について詳しく知る必要があります。しかし、当記事ではパッケージマネージャとしてYarnを用います。yarnは以下からダウンロードできます。MobXの前に、軽くReactに関する知識が必要です。
- Hello World|React を確認しておきましょう。JSXを知り、関数型コンポーネントが書けるようになればオッケーです。
- 今回のチュートリアルでは、関数型コンポーネントとHooksをたくさん書くので、以下の大変参考になるQiita記事を目を通しておくかもいいかもしれません。なお、本チュートリアルにおいてはHooksは、都度説明を入れるつもりです。React 16.8: 正式版となったReact Hooksを今さら総ざらいする|Qiita by uhyo
1.準備
Next.js 9を使って、今回のチュートリアルの環境を作っていきましょう。
パッケージマネージャと依存関係のインストール
作業用ディレクトリを作成し、そこで以下のコマンドを実行することで依存関係(パッケージ)をインストールします。
terminalyarn add next@latest react@latest react-dom@latest mobx mobx-react-lite
執筆当時のpackage.json
package.json{ "dependencies": { "mobx": "^5.11.0", "mobx-react-lite": "^1.4.1", "next": "^9.0.1", "react": "^16.8.6", "react-dom": "^16.8.6" } }開発サーバの立ち上げ
pages
という名前のディレクトリを作成し、その中にindex.jsx
というファイルを作成します。pages/index.jsxconst Index = () => <p>It Works!</p>; export default Index;そして、以下のコマンドを実行すると開発サーバが立ち上がります。
terminalyarn next
そして、http://localhost:3000 にアクセスした時に以下のように表示されていたら成功です。
開発サーバーはターミナルで
ctr+c
でを打てば終了します。Next.jsの基本
Next.js
はpages/
配下の.jsx
ファイルなどでReact
のコンポーネントをexport default
すると、そのディレクトリ名に対応するページが新規作成されます。また、開発サーバーにおける
Next.js
はファイルの更新を検出し、サーバーを再起動することなく更新を反映させます。次のチュートリアルの準備のために、
pages/counter.jsx
を作成して中をこのようにします。pages/counter.jsxconst CounterPage = () => { return ( <div> <p>ここはカウンターページです</p> <hr/> </div> ); }; export default CounterPage;すると先ほどの説明のように、counterというページがブラウザで読み込めるようになります。
http://localhost:3000/counter
2.Hello MobX
MobXの観測可能な状態と観測者を早速使ってみましょう。
pages/counter.jsximport {Observer, useLocalStore} from "mobx-react-lite"; //追加 const CounterPage = () => { /*** * 観測可能な状態 * ストア:{counter: number}のように扱える */ const store = useLocalStore(() => ({counter: 0})); /*** * ストアの操作のための関数 * ボタンに与える */ function increment() { store.counter++; } function decrement() { store.counter--; } return ( <div> <p>ここはカウンターページです</p> <hr/> {/*** * 観測者コンポーネント * 観測可能な状態の変化に応じて更新される */} <Observer>{() => (<p>{store.counter}</p>)}</Observer> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> </div> ); }; export default CounterPage;これで、+を押せばカウントアップされ、-を押せばカウントダウンするページが作れました。
解説
観測可能な状態
mobx-react-lite
ではuseLocalStore
Hookを使えば、観測可能な状態を作ることができます。観測可能な状態の作り方は他にもあります。useLocalStore(() => {return オブジェクト}); // これでオブジェクト型のObservableステートが作れる
useLocalStore
はHookですので、二つのルールがあります。フックは JavaScript の関数ですが、2 つの追加のルールがあります。
- フックは関数のトップレベルのみで呼び出してください。ループや条件分岐やネストした関数の中でフックを呼び出さないでください。
- フックは React の関数コンポーネントの内部のみで呼び出してください。通常の JavaScript 関数内では呼び出さないでください(ただしフックを呼び出していい場所がもう 1 カ所だけあります — 自分のカスタムフックの中です。これについてはすぐ後で学びます)。
したがって、クラス型のコンポーネントからは
useLocalStore
Hookは使えません。観測者
mobx-react-lite
では観測者Reactコンポーネントを作ることができます。Observerコンポーネント
は、子要素のようにReactのコンポーネントを書くことができず、render関数を渡してやる必要があります。つまりObserverコンポーネント
を使う際は以下の形式になることが多いでしょう。<Observer>{() => (観測可能な状態にアクセスするReact要素)}</Observer>観測可能な状態へのアクセスとは、短絡的な話だと
観測可能な状態.観測可能な状態のメンバー
における.
を含むということです。3. mobx-react-liteで提供される観測者(HOC, Observerコンポーネント, useObserver)
ちょっと準備:Hooksやコンポーネントを使い回せるようにする
先ほどのチュートリアルで
useLocalStore
によるカウンターのストアを作りました。これを使い回せるようにカスタムHookを作成しましょう。hooks/counterStore.jsimport { useLocalStore } from "mobx-react-lite"; export function useCounterStore() { const store = useLocalStore(() => ({ counter: 0, increment: () => { store.counter++; }, decrement: () => { store.counter--; } })); return store; }incrementや、decrementをStoreの中に入れてしまうことで、取り回しが良くなります。
次にボタンもコンポーネントにしましょう。
components/counterButton.jsxexport const CounterButton = props => ( <> <button onClick={props.store.increment}>+</button> <button onClick={props.store.decrement}>-</button> </> );ここまででファイル構成はこのようになっています。
. ├── components │ └── counterButton.jsx ├── hooks │ └── useCounterStore.js ├── package.json ├── pages │ ├── counter.jsx │ └── index.jsx └── yarn.lockObserverコンポーネント
Observerコンポーネントは最もよく使う観測者でしょう。使い方はすでに見た通りです。
pages/observer.jsxの全体
pages/observer.jsximport { Observer } from "mobx-react-lite"; import { useCounterStore } from "../hooks/useCounterStore"; import { CounterButton } from "../components/counterButton"; const Counter = props => <p>{props.counter}</p>; const CounterPage = () => { const store = useCounterStore(); return ( <div> <p>オブザーバーコンポーネントの例</p> <hr /> <Observer>{() => <Counter counter={store.counter} />}</Observer> <CounterButton store={store} /> </div> ); }; export default CounterPage;Observerコンポーネントでハマるとすればrender propsです。
Observerコンポーネントが動かない例
Observerコンポーネントは、直下のrender関数の更新をすることができますが、render関数中でさらにrender関数を呼び出された場合、子のrender関数の更新をすることができません。したがって以下のような例ではカウンターが動きません。
動かない例
<Observer> {() => ( <Ueshita render={props => ( <> ここは{props.name} <Counter counter={store.counter} /> </> )} > {props => ( <> ここは{props.name} <Counter counter={store.counter} /> </> )} </Ueshita> )} </Observer>動く例
<Ueshita render={props => ( <> ここは{props.name} <Observer>{() => <Counter counter={store.counter} />}</Observer> </> )} > {props => ( <> ここは{props.name} <Observer>{() => <Counter counter={store.counter} />}</Observer> </> )} </Ueshita>
pages/observer2.jsxの全体
pages/observer2.jsximport { Observer } from "mobx-react-lite"; import { useCounterStore } from "../hooks/useCounterStore"; import { CounterButton } from "../components/counterButton"; const Counter = props => <p>{props.counter}</p>; const Ueshita = props => ( <div> {props.render({ name: "上" })} <hr /> {props.children({ name: "下" })} </div> ); const CounterPage = () => { const store = useCounterStore(); return ( <div> <p>オブザーバーHOCの例</p> <hr /> <h2>動かない</h2> <Observer> {() => ( <Ueshita render={props => ( <> ここは{props.name} <Counter counter={store.counter} /> </> )} > {props => ( <> ここは{props.name} <Counter counter={store.counter} /> </> )} </Ueshita> )} </Observer> <hr /> <h2>動く</h2> <Ueshita render={props => ( <> ここは{props.name} <Observer>{() => <Counter counter={store.counter} />}</Observer> </> )} > {props => ( <> ここは{props.name} <Observer>{() => <Counter counter={store.counter} />}</Observer> </> )} </Ueshita> <CounterButton store={store} /> </div> ); }; export default CounterPage;参考: https://mobx-react.netlify.com/observer-component
observer HOC
HOCはコンポーネントを引数として、コンポーネントを返す関数です。mobx-react-liteには、ただのコンポーネントを受け取って、それを観測者にする
observer
というHOCがあります。先ほど出てきたObserver
は先頭が大文字です。注意してください。pages/hoc.jsximport { observer } from "mobx-react-lite"; import { useCounterStore } from "../hooks/useCounterStore"; import { CounterButton } from "../components/counterButton"; const Counter = props => <p>{props.store.counter}</p>; const HOCCounter = observer(Counter); const CounterPage = () => { const store = useCounterStore(); return ( <div> <p>オブザーバーHOCの例</p> <HOCCounter store={store}/> <hr /> <CounterButton store={store} /> </div> ); }; export default CounterPage;これは、先ほどの例と同じようにカウンターとして機能します。
落とし穴: observerが機能しない
ちょっと待ってください。 この例のCounterコンポーネントのpropsの取り方が少し冗長なように見えます。この機能を実装するならばいちいちstoreを渡さなくても良さそうに思えますね。つまりこちらの方が汎用性が高いコンポーネントでしょう。
const CounterMod = props => <p>{props.counter}</p>;これをobserver HOCに繋ぎます。
const HOCCounterMod = observer(CounterMod);そして表示してみましょう。比較用に、Observerコンポーネントに直繋ぎする例も見てみます。
<> カウンターModを直にオブサーバーにつなぐ例 <Observer>{() => <CounterMod counter={store.counter} />}</Observer> 単にHOCで繋いだ例 <HOCCounterMod counter={store.counter}/> </>
pages/hoc.jsxの全体
pages/hoc.jsximport { observer, Observer } from "mobx-react-lite"; import { useCounterStore } from "../hooks/useCounterStore"; import { CounterButton } from "../components/counterButton"; const Counter = props => <p>{props.store.counter}</p>; const HOCCounter = observer(Counter); const CounterMod = props => <p>{props.counter}</p>; const HOCCounterMod = observer(CounterMod); const CounterPage = () => { const store = useCounterStore(); return ( <div> <p>オブザーバーHOCの例</p> <HOCCounter store={store} /> <hr /> カウンターModを直にオブサーバーにつなぐ例 <Observer>{() => <CounterMod counter={store.counter} />}</Observer> 単にHOCで繋いだ例 <HOCCounterMod counter={store.counter}/> <CounterButton store={store} /> </div> ); }; export default CounterPage;なんと、動かない例が出てしまいました!単にHOCに繋いだものが更新されないのです。
これを動く例にしてみましょう。以下の二つを追加してみます
const HOCCounterModFixed = observer(props => <CounterMod counter={props.store.counter}/>);<> StoreからアクセスするようにHOCで繋いだ例 <HOCCounterModFixed store={store}/> </>
pages/hoc.jsxの全体
pages/hoc.jsximport { observer, Observer } from "mobx-react-lite"; import { useCounterStore } from "../hooks/useCounterStore"; import { CounterButton } from "../components/counterButton"; const Counter = props => <p>{props.store.counter}</p>; const HOCCounter = observer(Counter); const CounterMod = props => <p>{props.counter}</p>; const HOCCounterMod = observer(CounterMod); const HOCCounterModFixed = observer(props => <CounterMod counter={props.store.counter}/>); const CounterPage = () => { const store = useCounterStore(); return ( <div> <p>オブザーバーHOCの例</p> <HOCCounter store={store} /> <hr /> カウンターModを直にオブサーバーにつなぐ例 <Observer>{() => <CounterMod counter={store.counter} />}</Observer> 単にHOCで繋いだ例 <HOCCounterMod counter={store.counter}/> StoreからアクセするようにHOCで繋いだ例 <HOCCounterModFixed store={store}/> <CounterButton store={store} /> </div> ); }; export default CounterPage;これで予想通りに動くようになりました。
落とし穴の理由
MobXにおいて観測者が追跡しているものは観測可能な状態へのアクセスであり、シンプルな言い方をすれば
観測可能な状態.観測可能な状態のメンバー
における.
を追っているのです。const CounterMod = props => <p>{props.counter}</p>; const HOCCounterMod = observer(CounterMod);つまり
const HOCCounterMod = observer(props => <p>{props.counter}</p>);を
<HOCCounterMod counter={store.counter}/>と使ったところで、
observer
の引数の中でstore.counter
の.
が見えていません。MobXは観測可能な状態のメンバーそのもの、つまり値そのものの変化に対して反応することはできません。
したがって
observer
HOCにおいては、propsで観測可能な状態を渡すようにしましょう。観測可能な状態のメンバーをコピーしたものや、観測可能な状態にアクセスした後の値を渡しても、表示は更新されません。つまりHOCオブジェクトにおいてはJSXの要素の中で
.
があってもダメなのです。<HOCCounterMod counter={store.counter}/>この仕様のため、慣れてくると多くの場合で
Observer
コンポーネントを使うことが一番都合が良いと思うようになってきます。つまり、以下の内容はちゃんと値の変化に応じて描画されます。
<Observer>{() => <HOCCounterMod counter={store.counter}/> />}</Observer>こんなことをするのならば、observer HOCを使った意味がありませんね。
とはいえ、observer HOCを使えばこのようなことができます。
pages/hoc2.jsximport { observer } from "mobx-react-lite"; import { useCounterStore } from "../hooks/useCounterStore"; import { CounterButton } from "../components/counterButton"; const Counter = props => <p>{props.counter}</p>; const CounterPage = observer(() => { const store = useCounterStore(); return ( <div> <p>オブザーバーHOCの例2</p> <hr /> <Counter counter={store.counter} /> <CounterButton store={store} /> </div> ); }); export default CounterPage;これはちゃんと動きます。しかし再描画の範囲がCounterだけではなく、CounterPage全体ということに注意をしてください。
やたらobserver HOCをディスリましたが、使い用はあるはずです。
でも、もう一つディスりポイントがあって、observer HOCはReactのLegacy Contextに依存しています。その点でもobserver HOCは気味が悪いですね。
参考: https://mobx-react.netlify.com/observer-hoc
useObserver Hook
useObserver Hookは前述の二つの観測者で内部的に利用されているReact Hookです。observer HOCの代わりのような使い方ができます。
実際に現在のObserverコンポーネントの実装(TypeScript)は以下のようになっています。
function ObserverComponent({ children, render }: IObserverProps) { const component = children || render if (typeof component !== "function") { return null } return useObserver(component) }useObserverはmobx-react-liteの心臓部と言って差し支えないでしょう。
参考: https://github.com/mobxjs/mobx-react-lite/blob/master/src/ObserverComponent.ts
useObserver
をobserver
HOCのように使いたければ以下のようにしましょう。pages/useobserver.jsximport { useObserver } from "mobx-react-lite"; import { useCounterStore } from "../hooks/useCounterStore"; import { CounterButton } from "../components/counterButton"; const Counter = props => <p>{props.counter}</p>; const HookCounter = props => { //普通はこれをそのままreturnでオッケー:Hookっぽさを出すためにこうした。 const Component = useObserver(() => ( <Counter counter={props.store.counter} /> )); return Component; }; const CounterPage = () => { const store = useCounterStore(); return ( <div> <h2>オブザーバーHOOKの例</h2> <HookCounter store={store} /> <hr /> <CounterButton store={store} /> </div> ); }; export default CounterPage;まとめ
- 基本はObserverコンポーネントを利用しましょう。
- 観測者が動かない時は、観測可能な状態からのアクセスを観測できているかチェックする。
- Render Propsで動かない時は、Observerをrender関数の中に入れる