なんとなくReactのチュートリアルをTypeScript JSXでこなせるような気がしてきたので、やってみたいと思います。
プロジェクトの初期設定とかは以下の記事を参照してください。
最初のコンポーネント
最初は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を読み込ませます。
<!DOCTYPE html><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に公開しておきます