なんとなくReactのチュートリアルをTypeScript JSXでこなせるような気がしてきたので、やってみたいと思います。
facebook.github.io
プロジェクトの初期設定とかは以下の記事を参照してください。
blog.okazuki.jp
最初のコンポーネント
最初はCommentBoxという名前でHello worldをしてるような感じなのでさくっといきましょう。
<reference path="typings/tsd.d.ts" />class CommentBox extends React.Component<any, any> {
render() {
return<div className="commentBox">
Hello, world!I am a CommentBox.
</div>;
}
}
ReactDOM.render(
<CommentBox />,
document.getElementById("content"));
コンポーネントの組み立て
次は、コンポーネントを複数作って組み合わせるフェーズです。各コンポーネントの中身は、まだHello worldです。
<reference path="typings/tsd.d.ts" />class CommentList extends React.Component<any, any> {
render() {
return<div className="commentList">
Hello, world! I am a CommentList.
</div>;
}
}
class CommentForm extends React.Component<any, any> {
render() {
return<div className="commentForm">
Hello, world! I am a CommentForm.
</div>;
}
}
class CommentBox extends React.Component<any, any> {
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList />
<CommentForm />
</div>;
}
}
ReactDOM.render(
<CommentBox />,
document.getElementById("content"));
Props を使う
Commentを定義してpropsの使い方を示している箇所です。ただ、Commentっていうクラスを定義すると重複してる(何と?)と怒られたのでCommentItemという名前のクラスにしました。そこだけオリジナルチュートリアルと違います。
interface CommentItemProps extends React.Props<any> {
author:string;
}
class CommentItem extends React.Component<CommentItemProps, any> {
render() {
return<div className="commentItem">
<h2 className="commentAuthor">{this.props.author}</h2>
{this.props.children}
</div>;
}
}
コンポーネントのプロパティ
先ほど定義したCommentItemをCommentListに埋め込んでいます。
class CommentList extends React.Component<any, any> {
render() {
return<div className="commentList">
<CommentItem author="Pete Hunt">This is one comment</CommentItem>
<CommentItem author="Jordan Walke">This is *another* comment</CommentItem>
</div>;
}
}
Markdown の追加
markedというライブラリを使うみたいなので、パッケージマネージャーコンソールに以下のコマンドを打ち込んでJSのファイルとd.ts(あってよかった)インストールして、ソリューションエクスプローラですべてのファイルを表示するにしてから、プロジェクトに取り込みます。
PM> tsd install marked -save
PM> bower install marked
index.htmlにmarkedのjsを読み込ませます。
<htmllang="ja"><head><metacharset="utf-8" /><title>React tutorial</title></head><body><divid="content"></div><scriptsrc="bower_components/jquery/dist/jquery.min.js"></script><scriptsrc="bower_components/react/react.js"></script><scriptsrc="bower_components/react/react-dom.min.js"></script><scriptsrc="bower_components/marked/marked.min.js"></script><scriptsrc="app.js"></script></body></html>
CommentItemをmarkedを使って書き直します。
class CommentItem extends React.Component<CommentItemProps, any> {
render() {
return<div className="commentItem">
<h2 className="commentAuthor">{this.props.author}</h2>
{marked(this.props.children.toString())}
</div>;
}
}
この状態では、サニタイズされてHTMLのタグが、そのままブラウザ上に表示されてしまうため、サニタイズしないように変更します。
class CommentItem extends React.Component<CommentItemProps, any> {
render() {
var rawMarkup = marked(this.props.children.toString());
return<div className="commentItem">
<h2 className="commentAuthor">{this.props.author}</h2>
<span dangerouslySetInnerHTML={{ __html: rawMarkup }}></span>
</div>;
}
}
データモデルとの連携
まずは、データを用意します。インターフェースでタイプセーフに定義できるようにしてみましょう。
interface Data {
author:string;
text:string;
}
var data: Data[] = [
{ author: "Pete Hunt", text: "This is one comment" },
{ author: "Jordan Wakle", text: "This is *another* comment" }
];
そして、CommentBoxとCommentListがdataというプロパティを受け取るようになったので、変更します。
<reference path="typings/tsd.d.ts" />interface Data {
author:string;
text:string;
}
var data: Data[] = [
{ author: "Pete Hunt", text: "This is one comment" },
{ author: "Jordan Wakle", text: "This is *another* comment" }
];
interface CommentItemProps extends React.Props<any> {
author:string;
}
class CommentItem extends React.Component<CommentItemProps, any> {
render() {
var rawMarkup = marked(this.props.children.toString());
return<div className="commentItem">
<h2 className="commentAuthor">{this.props.author}</h2>
<span dangerouslySetInnerHTML={{ __html: rawMarkup }}></span>
</div>;
}
}
interface CommentListProps extends React.Props<any> {
data: Data[];
}
class CommentList extends React.Component<CommentListProps, any> {
render() {
return<div className="commentList">
<CommentItem author="Pete Hunt">This is one comment</CommentItem>
<CommentItem author="Jordan Walke">This is *another* comment</CommentItem>
</div>;
}
}
class CommentForm extends React.Component<any, any> {
render() {
return<div className="commentForm">
Hello, world! I am a CommentForm.
</div>;
}
}
interface CommentBoxProps extends React.Props<any> {
data: Data[];
}
class CommentBox extends React.Component<CommentBoxProps, any> {
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>;
}
}
ReactDOM.render(
<CommentBox data={data} />,
document.getElementById("content"));
そして、データを使って動的にレンダリングするようにします。
class CommentList extends React.Component<CommentListProps, any> {
render() {
var commentNodes = this.props.data.map(x => <CommentItem author={x.author}>{x.text}</CommentItem>);
return<div className="commentList">
{commentNodes}
</div>;
}
}
サーバからのデータの取得
CommentBoxのプロパティをurlにしてしまいます。
interface CommentBoxProps extends React.Props<any> {
url:string;
}
class CommentBox extends React.Component<CommentBoxProps, any> {
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.props.data} />
<CommentForm />
</div>;
}
}
ReactDOM.render(
<CommentBox url="api/comments.json" />,
document.getElementById("content"));
この状態では、まだCommentBoxにコンパイルエラーがあります。なおしていきましょう。
Reactive state
CommentBoxにStateを定義します。インターフェースを新たに切って型引数に指定します。
そしてコンストラクタでStateの初期値を指定します。
interface CommentBoxProps extends React.Props<any> {
url:string;
}
interface CommentBoxState {
data: Data[];
}
class CommentBox extends React.Component<CommentBoxProps, CommentBoxState> {
constructor(props: CommentBoxProps) {
super(props);
this.state = { data: [] };
}
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>;
}
}
そして、データをサーバーから読み込むようにします。
interface CommentBoxProps extends React.Props<any> {
url:string;
}
interface CommentBoxState {
data: Data[];
}
class CommentBox extends React.Component<CommentBoxProps, CommentBoxState> {
constructor(props: CommentBoxProps) {
super(props);
this.state = { data: [] };
}
componentDidMount() {
$.ajax({
url:this.props.url,
dataType:"json",
cache:false,
success: (data => this.setState({ data: data } as CommentBoxState)).bind(this),
error: ((xhr, status, err) => console.error(this.props.url, status, err.toString())).bind(this)
});
}
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>;
}
}
続けて、コメントを2秒間隔で読み込む変更を加えます。
interface CommentBoxProps extends React.Props<any> {
url:string;
poolInterval: number;
}
interface CommentBoxState {
data: Data[];
}
class CommentBox extends React.Component<CommentBoxProps, CommentBoxState> {
constructor(props: CommentBoxProps) {
super(props);
this.state = { data: [] };
}
private loadCommentsFromServer() {
$.ajax({
url:this.props.url,
dataType:"json",
cache:false,
success: (data => this.setState({ data: data } as CommentBoxState)).bind(this),
error: ((xhr, status, err) => console.error(this.props.url, status, err.toString())).bind(this)
});
}
componentDidMount() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer.bind(this), this.props.poolInterval);
}
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm />
</div>;
}
}
ReactDOM.render(
<CommentBox url="api/comments.json" poolInterval={2000} />,
document.getElementById("content"));
setIntervalの第一引数のメソッドを渡すときはbindでthisを固定してあげないとエラーになります。
これで、サーバーのデータを更新すると画面が2秒以内に更新されるようになりました。
新しいコメントの追加
refsの書き方が直感的でないことを除けば普通のTypeScriptでいけます。
class CommentForm extends React.Component<any, any> {
private handleSubmit(e: React.FormEvent) {
e.preventDefault();
var author = (ReactDOM.findDOMNode(this.refs["author"]) as HTMLInputElement).value.trim();
var text = (ReactDOM.findDOMNode(this.refs["text"]) as HTMLInputElement).value.trim();
if (!text || !author) {
return;
}
TODO
(ReactDOM.findDOMNode(this.refs["author"]) as HTMLInputElement).value = "";
(ReactDOM.findDOMNode(this.refs["text"]) as HTMLInputElement).value = "";
return;
}
render() {
return<form className="commentForm" onSubmit={this.handleSubmit.bind(this)}>
<input type="text" placeholder="your name"ref="author" />
<input type="text" placeholder="Say something..."ref="text" />
<input type="submit"value="Post" />
</form>;
}
}
Props としてのコールバック
CommentFormのPropsにコールバックを定義して、それをhandleSubmitで呼び出します。
interface CommentFormProps extends React.Props<any> {
onCommentSubmit: (data: Data) => void;
}
class CommentForm extends React.Component<CommentFormProps, any> {
private handleSubmit(e: React.FormEvent) {
e.preventDefault();
var author = (ReactDOM.findDOMNode(this.refs["author"]) as HTMLInputElement).value.trim();
var text = (ReactDOM.findDOMNode(this.refs["text"]) as HTMLInputElement).value.trim();
if (!text || !author) {
return;
}
this.props.onCommentSubmit({ author: author, text: text } as Data);
(ReactDOM.findDOMNode(this.refs["author"]) as HTMLInputElement).value = "";
(ReactDOM.findDOMNode(this.refs["text"]) as HTMLInputElement).value = "";
return;
}
render() {
return<form className="commentForm" onSubmit={this.handleSubmit.bind(this) }>
<input type="text" placeholder="your name"ref="author" />
<input type="text" placeholder="Say something..."ref="text" />
<input type="submit"value="Post" />
</form>;
}
}
そして、CommentBoxで定義しているCommentFormに対してonCommentSubmitを指定します。
class CommentBox extends React.Component<CommentBoxProps, CommentBoxState> {
constructor(props: CommentBoxProps) {
super(props);
this.state = { data: [] };
}
private loadCommentsFromServer() {
$.ajax({
url:this.props.url,
dataType:"json",
cache:false,
success: (data => this.setState({ data: data } as CommentBoxState)).bind(this),
error: ((xhr, status, err) => console.error(this.props.url, status, err.toString())).bind(this)
});
}
private handleCommentSubmit(comment: Data) {
TODO
}
componentDidMount() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer.bind(this), this.props.poolInterval);
}
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
</div>;
}
}
Web APIをたたいてコメントを登録するようにします…が、コメント投稿APIがないのでエラーになってしまいます。
class CommentBox extends React.Component<CommentBoxProps, CommentBoxState> {
constructor(props: CommentBoxProps) {
super(props);
this.state = { data: [] };
}
private loadCommentsFromServer() {
$.ajax({
url:this.props.url,
dataType:"json",
cache:false,
success: (data => this.setState({ data: data } as CommentBoxState)).bind(this),
error: ((xhr, status, err) => console.error(this.props.url, status, err.toString())).bind(this)
});
}
private handleCommentSubmit(comment: Data) {
$.ajax({
url:this.props.url,
dataType:'json',
type:'POST',
data: JSON.stringify(comment),
success: (data => this.setState({ data: data } as CommentBoxState)).bind(this),
error: ((xhr, status, err) => console.error(this.props.url, status, err.toString())).bind(this)
});
}
componentDidMount() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer.bind(this), this.props.poolInterval);
}
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
</div>;
}
}
最適化: 先読み更新
残るは先読みですが、ここからは動作検証できてないので間違ってるかもしれません。
class CommentBox extends React.Component<CommentBoxProps, CommentBoxState> {
constructor(props: CommentBoxProps) {
super(props);
this.state = { data: [] };
}
private loadCommentsFromServer() {
$.ajax({
url:this.props.url,
dataType:"json",
cache:false,
success: (data => this.setState({ data: data } as CommentBoxState)).bind(this),
error: ((xhr, status, err) => console.error(this.props.url, status, err.toString())).bind(this)
});
}
private handleCommentSubmit(comment: Data) {
var comments = this.state.data;
var newComments = comments.concat([comment]);
this.setState({ data: newComments } as CommentBoxState);
$.ajax({
url:this.props.url,
dataType:'json',
type:'POST',
data: JSON.stringify(comment),
success: (data => this.setState({ data: data } as CommentBoxState)).bind(this),
error: ((xhr, status, err) => {
this.setState({ data: comments } as CommentBoxState);
console.error(this.props.url, status, err.toString());
}).bind(this)
});
}
componentDidMount() {
this.loadCommentsFromServer();
setInterval(this.loadCommentsFromServer.bind(this), this.props.poolInterval);
}
render() {
return<div className="commentBox">
<h1>Comments</h1>
<CommentList data={this.state.data} />
<CommentForm onCommentSubmit={this.handleCommentSubmit.bind(this)} />
</div>;
}
}
これで一通りTypeScriptでReactのチュートリアルのコードを書き直したことになります。最後、動作確認できないのがつらいところですが、まぁ雰囲気がつかめたのでよしとしましょう。
それにしてもrefsの書き心地だけが果てしなく悪い…。
リポジトリ
一応GitHubに公開しておきます
github.com