かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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

なんとなく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>

<html lang="ja">
<head>
    <meta charset="utf-8" />
    <title>React tutorial</title>
</head>
<body>
    <div id="content"></div>
    <script src="bower_components/jquery/dist/jquery.min.js"></script>
    <script src="bower_components/react/react.js"></script>
    <script src="bower_components/react/react-dom.min.js"></script>
    <script src="bower_components/marked/marked.min.js"></script>
    <script src="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