React使ってSPAを作るよ(15)

React使ってSPAを作るよ(15)

React使ってSPAを作るよ目次

React使ってSPAを作るよ(14)の続きです。

今回やることは、共通のコンポーネントを使って、タグの一覧をヘッダにもレンダリングすること。

さて、いきなりですがUIを変えてみました。
モックはこちら。

See the Pen dMNavJ by natsumi (@mayo31) on CodePen.18228

新規タブでフルページを開く

※全体的にピンクになっているのは同僚がAKBファンで、ピンクにしてくれってことだったので、もらったカラーコードを使うことになりました。

ときどきこうやってモックのHTML構造を変えたり、JSONのキーを変えたりしてるところもあるので、過去の記事のソースをそのままコピペして試している方がいたら気を付けてください。

ヘッダのフォームと、タグの絞り込みリストをドロワーにしました。
ボタンのアイコンはsvgでそれらしいものを作ってみました。
アニメーションをちゃんと設定していないので細かい調整は必要ですが、やりたいことのイメージはつかんでもらえるかと思います。

Reactのコンポーネント志向が活かされる、パーツの再利用

今回、タグで絞り込めるボタンのリストは、2箇所あります。
ヘッダにあるタグ絞り込みリストと、記事に紐づけられたタグのリストです。

どちらも同じ.tag-listで、同じスタイルがあたっている同じHTML構造のリストです。

これまではヘッダのリストはなかったので、ArticleArea.jsxの中にタグコンポーネントを書いていました。
でも、同じソースなんだし、2回書くのは嫌ですね。
共通で利用できたらいいですよね。

とりあえず管理しやすいように、いったんTag.jsxを作ってしまいましょう。

var React = require('react');

//タグ
var Tag = React.createClass({
  render: function() {
    return (
      <li><a name={this.props.id}</a></li>
    )
  }
});

//タグリスト
var TagList = React.createClass({
  render: function() {
    var tagNodes = this.props.data.map(function(tag) {
      return (
        <Tag tag={tag.name} id={tag.id} key={tag.id}/>
      )
    });
    return (
      <ul className="tag-list">
        {tagNodes}
      </ul>
    )
  }
});

module.exports = TagList;

ArticleArea.jsxに書いてあったtagコンポーネントやTagListコンポーネントを削除して、Tag.jsxからインポートするのを忘れずに。
同様に、今回追加するヘッダでも利用できるように、SiteHeader.jsxでも読み込んでおきましょう。

var TagList = require('./Tag.jsx');

で、今まではヘッダにはh1とURLFormコンポーネントしかなかったのですが、アイコンボタンのナビゲーションに変えたいので、ナビゲーションのコンポーネントをあらたに作ります。
ほとんどモックのHTMLをそのまま突っ込んでいます。

//ナビゲーションコンポーネント
var Navigation = React.createClass({
  render: function() {
    return (
      <nav id="menu">
        <input type="radio" name="menu" id="menu00" defaultChecked="checked" /><label htmlFor="menu00" className="overlay-close">×</label>
        <input type="radio" name="menu" id="menu01"/><label htmlFor="menu01">
          <svg viewBox="0 0 31 37">
            <path d="M26.5,36.5h-22c-2.2,0-4-1.8-4-4v-28c0-2.2,1.8-4,4-4h22
          c2.2,0,4,1.8,4,4v28C30.5,34.7,28.7,36.5,26.5,36.5z M27.5,18.5v-12c0-1.1-0.9-2-2-2h-20c-1.1,0-2,0.9-2,2v12c0,1.1,0.9,2,2,2h20
          C26.6,20.5,27.5,19.6,27.5,18.5z M4.475,26.291h22 M4.475,31.291h22"/>
          </svg>
        </label>
        <URLForm />
        <input type="radio" name="menu" id="menu02" /><label htmlFor="menu02">
          <svg viewBox="0 0 42.387 43.79">
            <path d="M30.5,15.5c0,8.284-6.716,15-15,15s-15-6.716-15-15s6.716-15,15-15
              S30.5,7.216,30.5,15.5z M15.5,5.5c-5.523,0-10,4.477-10,10s4.477,10,10,10s10-4.477,10-10S21.023,5.5,15.5,5.5z M37.966,39.972
              l-7.221-9.585c-0.993-1.318-2.883-1.584-4.201-0.591l0,0c-1.318,0.993-1.584,2.883-0.591,4.201l7.221,9.585
              c0.993,1.318,2.883,1.584,4.201,0.591l0,0C38.693,43.18,38.959,41.29,37.966,39.972z"/>
          </svg>
        </label>
        <TagList data={this.props.data} />
      </nav>
    );
  }
});

この中で、URLFormコンポーネントと、TagListコンポーネントを呼び出していますね。
TagListに渡すデータは、このあと親のコンポーネントでとってきますので少々お待ちを。

ReactでJSXを書くときの注意点いくつか

Navigationコンポーネントの中には、HTMLしか書けないWebデザイナー的ハマりポイントがたくさんあるので、JSXで書くときに修正しないといけないところをいくつか紹介します。
(勉強していくうちに追記するかもしれません。)

ラジオボタンのchecked属性
defaultCheckedにします。

labelのfor属性
htmlForにします。

inputのvalue属性
defaultValueにします。

input、imgなどの空要素
ベースがHTML5であっても、XMLと同じように、/をつけて閉じます。

SVGのxml絡みの属性
Illustratorで書き出したSVGコードからxml絡みの属性はすべて削除します。
コンパイルできないぞ、ってなっちゃうので…
fillやstroke、サイズなどのスタイル指定はCSS側でしています。

Reactを紹介している記事を見ていると、「JSXならWebデザイナーでも修正しやすい!」というメリットが多く見られるのですが、実際にはこのようにJSXの記法に慣れる必要があります。

子コンポーネントにデータを渡す

ということでNavigationコンポーネントができたら…
今度はSiteHeaderコンポーネントで呼び出しましょう。
以前はURLFormが入っていたところですが、ここをNavigationにしておきます。

  render: function() {
    return (
      <header id="common-header">
        <h1>ヘッダh1</h1>
        <Navigation data={this.state.data}/>
      </header>
    );
  }
});

また、Navigationコンポーネントの中で使うタグリストのデータを渡すために、data={this.state.data}をつけています。
じゃあこのdataってのはどうやって取ってくるのよ、というと…
これまでにやったのと同じように、APIを叩いてJSON形式のデータを取得します。

var SiteHeader = React.createClass({
  //JSONデータ取得
  getInitialState: function() {
    return {data: []};
  },
  loadArticleFromServer: function() {
    var t = this;
    $.ajax({
      url: './api/tag',
      dataType: 'json',
      cache: false,
    }).done(function(data) {
      t.setState({data: data});
    }).fail(function(xhr, status, err) {
      console.error('./api/tag', status, err.toString());
    });
  },
  componentDidMount: function() {
    this.loadArticleFromServer();
    setInterval(this.loadArticleFromServer, 2000);
  },
  
  render: function() {
    return (
      <header id="common-header">
        <h1>ヘッダh1</h1>
        <Navigation data={this.state.data}/>
      </header>
    );
  }
});

ちなみに今のAPIだと、こんな感じのJSONが返ってきます。

[
  {"id":"11","name":"SPA"},
  {"id":"10","name":"REACT"},
  {"id":"9","name":"REST"},
  {"id":"8","name":"PHP"},
  {"id":"7","name":"\u30c6\u30b9\u30c8"}
]

ここでとってくるデータは、ArticleAreaの中のリストとは異なります。
ArticleAreaでとってくるデータは、ある記事に紐づいたタグのリストなので、

//ArticleArea.jsx

var React = require('react');
var TagList = require('./Tag.jsx');

//記事コンポーネント
var ArticleList = React.createClass({
  render: function() {
    var articleImage = {
      backgroundImage : "url(" + this.props.articleImage + ")" 
    };
    return (
    <li>
      <article>
        <a href={this.props.articleUrl} target="_blank" style={articleImage}>
          <h2>{this.props.articleTitle}</h2>
          <p>{this.props.articleDescription}</p>
        </a>
        <TagList data={this.props.articleTag} />
      </article>
    </li>
    );
  }
});

//記事リストコンポーネント
var ArticleArea = React.createClass({

  //JSONデータ取得
  getInitialState: function() {
    return {data: []};
  },
  loadArticleFromServer: function() {
    var t = this;
    $.ajax({
      url: './api/article',
      dataType: 'json',
      cache: false,
    }).done(function(data) {
      t.setState({data: data});
    }).fail(function(xhr, status, err) {
      console.error('./api/article', status, err.toString());
    });
    $('#spinner').css('display', 'none');
  },
  componentDidMount: function() {
    this.loadArticleFromServer();
    setInterval(this.loadArticleFromServer, 2000);
  },
  
  render: function() {
    var articleNodes = this.state.data.map(function(article) {
      return (
        <ArticleList articleTitle={article.articleTitle} articleUrl={article.articleUrl} articleDescription={article.articleDescription} articleImage={article.articleImage} articleTag={article.tagData} key={article.articleID}/>
      )
    });
    return (
    <ul id="article-list">
      {articleNodes}
    </ul>
    )
  }
});

module.exports = ArticleArea;

といった感じで、/api/articleという記事リストを返すAPIを叩いています。

こういう感じのJSONが返ってきています。

[
  {
    "articleID":"15",
    "articleTitle":"React\u4f7f\u3063\u3066SPA\u3092\u4f5c\u308b\u3088\uff0810\uff09 | Web\u30fb\u30a2\u30d7\u30ea\u5236\u4f5c\u30e1\u30e2 - m31",
    "articleUrl":"http:\/\/m31.fool.jp\/react%e4%bd%bf%e3%81%a3%e3%81%a6spa%e3%82%92%e4%bd%9c%e3%82%8b%e3%82%88%ef%bc%8810%ef%bc%89\/",
    "articleDescription":"React\u4f7f\u3063\u3066SPA\u3092\u4f5c\u308b\u3088\uff089\uff09\u306e\u7d9a\u304d\u3067\u3059\u3002\u30d8\u30c3\u30c0\u306b\u30d5\u30a9\u30fc\u30e0\u3092\u8ffd\u52a0\u3057\u3066\u3001\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u5225\u306b\u5206\u5272\u3057\u305fJSX\u30d5\u30a1\u30a4\u30eb\u3092\u5165\u308c\u5b50\u306b\u3057\u3066\u8aad\u307f\u8fbc\u3093\u3067\u3044\u304d\u307e\u3059\u3002",
    "articleImage":"http:\/\/m31.fool.jp\/wp-content\/uploads\/react.png",
    "tagData":[{"name":"REACT","id":"10"},{"name":"SPA","id":"11"}]
  },
  {
    "articleID":"13",
    "articleTitle":"Laravel5.2\u3067RESTful\u306aAPI\u3092\u4f5c\u3063\u3066\u307f\u308b",
    "articleUrl":"http:\/\/satobukuro.net\/111\/","articleDescription":"Laravel\u3067RESTful\u306aAPI\u3092\u4f5c\u6210\u3057\u3066\u307f\u307e\u3059\u3002DB\u3092\u4f7f\u3063\u3066\u307f\u308b\u306b\u3057\u3066\u3082\u3001\u307e\u305a\u306f\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u304b\u3089\u306a\u306e\u3067\u3002RESTful\u3063\u3066\u4f55\uff1f\u307e\u305a\u306f\u305d\u3053\u3067\u3059\u3088\u306d\u3002\u3082\u3046\u77e5\u3063\u3066\u308b\u3063\u3066\u4eba\u306f\u3053\u3053\u306f\u8aad\u307f\u98db\u3070\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u5927\u3057\u305f\u3053\u3068\u66f8\u3044\u3066\u3044\u306a\u3044\u306e\u3067\u3002\u8a73\u3057\u3044\u3053\u3068",
    "articleImage":"http:\/\/satobukuro.net\/wp-content\/uploads\/2016\/03\/shared_img_thumb_KENTA_863deadh_TP_V.jpg",
    "tagData":[{"name":"REST","id":"9"},{"name":"PHP","id":"8"}]
  },
  {
    "articleID":"12",

//略

この中に、記事に紐づいたタグのリスト、tagDataという配列を持っているのでこれをTagListに渡していますね。

<TagList data={this.props.articleTag} />

このTagListコンポーネント自体は、Navigationコンポーネントで使っているのと同じコンポーネントですが、渡すデータが別のものなので、生成されるリストも異なります。

こうやって、同じ構造のHTMLを共通のコンポーネントにして、再利用できるわけですね!

React使ってSPAを作るよ目次