Meteor and Reactによるリアクティブシステム「ラーメン野郎を追いかけろ! @ Twitter」を作ってみた
Falcor+Reactフルスタック(開発環境)
Falcor+Reactフルスタック(react-router)
Falcor+Reactフルスタック(views層とcomponents層)
Falcor+Reactフルスタック(formsy-react)
Falcor+Reactフルスタック(エラー処理)
Falcor+Reactフルスタック(Material-UI)
今回は、Reactのプログラム構成についてです。Reactはviewを作成するライブラリですが、一般的には複数のcomponentsから成り立ちます。ではどのようにしてcomponentsを作成して組み合わせていけばよいのでしょうか?
大雑把に言えば、一般的には1つの画面を作成するのに2つのcomponentsを作成します。2つのcomponentはviews層とcomponents層に属し親子関係にあります。viewsのcomponentが親で、components層のcomponentが子になります。
views層のcomponentは外部とのデータのやり取りやハンドラの定義を行い、データやハンドラをcomponents層のcomponentに渡します。components層のcomponentはより低レベルで具体的な画面を描画します。
具体的にユーザ登録画面で説明しましょう。ユーザ登録画面はviews層としてsrc/views/RegisterView.jsを使い、components層としてsrc/components/RegisterForm.jsを使います。この2ファイルで定義された2つcomponentでユーザ登録画面が作られています。
http://www.mypress.jp:3019/
import React from 'react';
import falcorModel from '../falcorModel.js';
import { RegisterForm } from '../components/RegisterForm.js';
export default class RegisterView extends React.Component {
constructor(props) {
super(props);
this.register = this.register.bind(this);
this.state = {
error: null
};
}
async register (newUserModel) {
await falcorModel
.call(['register'],[newUserModel])
.then((result) => result);
const newUserId = await falcorModel.getValue(['register', 'newUserId']);
if (newUserId === 'INVALID') {
const errorRes = await falcorModel.getValue('register.error');
this.setState({error: errorRes});
return;
}
this.props.history.push({pathname: '/login'});
}
render () {
return (
<div>
<h1>登録画面</h1>
<div style={{maxWidth: 450, margin: '0 auto'}}>
<RegisterForm
onSubmit={this.register} />
</div>
</div>
);
}
}
このviews層ではregister()というハンドラを作成し、components層の子component(RegisterForm)にonSubmitプロパティで渡しています。このハンドラは非同期処理(Falcor通信)を含んでいるので、async/awaitが使われています。これが起動されるのはcomponents層ですが、動作はviews層で行われます(あくまで概念的にですが)。views層で外部との通信(Falcor通信)が行われています。ユーザ登録が成功すれば、react-routerの機能を使ってlogin画面にリダイレクトします。これは前回の説明通りです。
this.props.history.push({pathname: '/login'});
import React from 'react';
import Formsy from 'formsy-react';
import { RaisedButton, Paper } from 'material-ui';
import DefaultInput from './DefaultInput';
export class RegisterForm extends React.Component {
constructor(props) {
super(props);
this.enableButton = this.enableButton.bind(this);
this.disableButton = this.disableButton.bind(this);
this.state = {
canSubmit: false
};
}
enableButton() {
this.setState({
canSubmit: true
});
}
disableButton() {
this.setState({
canSubmit: false
});
}
render() {
return (
<Formsy.Form onSubmit={this.props.onSubmit} onValid={this.enableButton} onInvalid={this.disableButton}>
<Paper zDepth={1} style={{padding: 32}}>
<h3>ユーザ登録フォーム</h3>
<DefaultInput name='username' title='ユーザ名' value="" required />
<DefaultInput name='firstName' title='名前' value="" required />
<DefaultInput name='lastName' title='苗字' value="" required />
<DefaultInput name='email' validations='isEmail' validationError='This is not a valid email' title='メール' value="" required />
<DefaultInput type='password' name='password' title='パスワード' value="" required />
<div style={{marginTop: 24}}>
<RaisedButton
disabled={!this.state.canSubmit}
secondary={true}
type="submit"
style={{margin: '0 auto', display: 'block', width: 150}}
label={'登録'} />
</div>
</Paper>
</Formsy.Form>
);
}
}
このcomponents層のcomponentは、JSXでユーザ登録画面を定義しているだけのシンプルなものです。通常components層のものはこのように画面の定義だけで、処理は親から受け取ったハンドラを起動するだけです。ここでの注意点としては、フォームの定義にFormsyライブラリを使っていることと、Input要素としてより低レベルのcomponents層のcomponentであるDefaultInputを使っていることです。このフォーム周りは次回以降に説明するつもりです。
前回の説明でRouteの定義を示しましたが、それぞれのパスが1画面に相当します。Routeで指定されたcomponentがviews層のcomponentであると言えます。PageNotFoundは例外ですが。
<Route exact path='/' component={HomeApp} name='home' />
<Route path='/login' component={LoginView} name='login' />
<Route path='/logout' component={LogoutView} name='logout' />
<Route path='/dashboard' component={DashboardView} name='dashboard' />
<Route path='/register' component={RegisterView} name='register' />
<Route component={PageNotFound}/>
views層では、falcor通信の他に、Redux Storeとのやり取りやminimongoとのやり取り(Meteorの場合)などが行われます。components層では親からデータを受け取り単純に画面を描画します。