Quantcast
Channel: かずきのBlog@hatena
Viewing all articles
Browse latest Browse all 1387

Visual Studio上のTypeScript JSXを使ってReact.js「ReactのチュートリアルをTypeScriptでリライト」

$
0
0

なんとなく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を読み込ませます。

<!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に公開しておきます

github.com


Viewing all articles
Browse latest Browse all 1387

Trending Articles