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

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

React使ってSPAを作るよ目次

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

前回までに、Reactの公式チュートリアルを参考にしてほぼリアルタイムで記事を反映する実装をしました。

…ですが、この方法だと2秒ごとにAPIを叩き続けることになってしまうのが気になります。
今はローカルで自分一人で閲覧していますが、これをサーバにあげて多数のユーザーがアクセスすることを考えると、必要なときだけ通信したいと思えてきます。

それからもうひとつ、実際にこういうサービスを稼働させて大勢が同時に使うようになり、リストが次々と更新されるようになったら…
クリックしようとしたときに再レンダリングされて、別の記事をクリックしちゃった!なんてことも起きたりするのではないか?そしたらイライラしないか?という、UXの観点でも疑問が残る実装になっています。

そう考えると、Twitter方式はあながち間違ってないかもしれませんね。(私はチャットみたいにどんどん流れていくのをボーっと見てる方が楽だなと思っていましたが)

単純に記事が更新されたのを受けて何かしたいとなるとPUSH型の通信を走らせるのが適切そうですが、先に挙げたように自動更新はやめたほうがよさそうです。

たとえばプッシュ通知で「新着記事があるよ!」というのを表示してユーザーがボタンを押すなどしたら手動でリフレッシュされる、という風にしたいとします。
だけど、自分がURLを送信した時は、それはその場で反映してほしいですよね。

というわけで、少しカスタマイズしてみたいと思います。
(今回はまだプッシュは実装しません)

Reactのチュートリアルに頼らず自分のサービスに合わせて実装しよう!

まず前回までの実装と変えなければいけないところ。
当然、ここですね。

    setInterval(this.loadArticleFromServer, 2000);

全部もとの状態に戻すのはちょっと待って。
URLを送信したときやタグをクリックしたときはこれまでのように$.ajaxで記事リストをレンダリングしたいので、loadArticleFromServer()はそのまま残しておきましょう。

こういうときはコードを修正する前に、どんな処理をすればやりたいことが実現できるかを先に考えます。
例えば…

  • 2秒ごとにloadArticleFromServer()するのはやめたい。
  • タグをクリックしたら叩くAPIのURLを変えたい。
  • URLが変わったことを認識させたい。
  • URLを送信したときは反映したい。
  • メイン画面でURLを送信したときはAPIのURLは変わらない。

などなど。

これを整理していくと、

タグをクリックしたら叩くAPIのURLを変えたい。
すでにAPIのURLを作る処理はできているので、そこは再利用できそう。

URLが変わったことを認識させたい。
今現在設定されているURLと比較できればできそう。
比較用の変数を増やしてURLを入れておこうかな?

2秒ごとにloadArticleFromServer()するのはやめたい。
URLが変わったことを認識させるために、setInterval()自体は使えそう。
URLが変わった時だけ、loadArticleFromServer()したら良さそう。

URLを送信したときは反映したい。
じゃあこのときもURLが変わっていれば反映できそうだよね。
でも…

メイン画面でURLを送信したときはAPIのURLは変わらない。
そうだね。
でもさ、比較する変数の中身が変わっていればいいんだから、送信したことがわかる値を入れておけばいいかも?
あ、次回比較するときのために変数をもとに戻しておかないといけないね。

といった感じです。

これを実際にコードにしていきます。

まずはタグのところ。

//タグ
var Tag = React.createClass({
  handleAjax: function() {
    apiURL = './api/article?tag=' + this.refs.tag.name;
    $('.overlay').removeClass('overlay');
    $('#overlay').css('display', 'none');
    $('#overlay-close').css('display', 'none');
    $('#spinner').css('display', 'block');
  },
  render: function() {
    return (
      <li><a ref="tag" name={this.props.id} onClick={this.handleAjax}>{this.props.tag}</a></li>
    )
  }
});

変更する必要がないですね。
タグのリストをクリックしたら、apiURLに叩くAPIのURLを代入するのは変わりません。
setInterval()側で処理が進んでくれるのでこのままにしておきましょう。

次に、「2秒ごとにURLを比較する」ためのコード。
これはもともとsetInterval()があるので、この中で実行する関数を変えるだけです。

  componentDidMount: function() {
    this.loadArticleFromServer();
    setInterval(this.fetchURL, 2000);
  },

ページを読み込んだ初回は、以前設定したように

var apiURL = './api/article';

となっているのでthis.loadArticleFromServer();で最初に設定したAPI(./api/article)を叩きます。

ですが、2秒後の比較のためにtmpURLにも同じ値が入っていないといけませんね。
最初に定義しておくのを忘れないようにしましょう。

var apiURL = './api/article';
var tmpURL = apiURL;

そして、setInterval()ではURLの比較を行うあらたな関数fetchURL(これも関数名はなんでもいいです)を2秒ごとに実行する設定にしておきます。

では、肝心のfetchURL()の中身ですが、ここでやることをおさらいしておくと

  • tmpURLとapiURLを比較する
  • 同じだったら何もしない
  • 違っていたら今のapiURLを叩いて記事リストを取得してくる
  • 終わったら次回の比較のためにtmpURLにapiURLの値を入れておく

ですね。

コードはこうなりました。

  fetchURL: function() {
    if(apiURL === 'post'){
      apiURL = './api/article';
      tmpURL = apiURL;
      this.loadArticleFromServer();
    }
    else if(apiURL !== tmpURL){
      tmpURL = apiURL;
      this.loadArticleFromServer();
    };
  },

URLを送信したときの比較用に「post」という文字列が入っていますが、これはURLを送信したときに下記の12行目で設定しています。

    $.ajax({
      url: './api/article',
      type: 'POST',
      data: {"url": inputURL, "tag": tagArr},
      timeout:10000,
      })
      .done(function() {
        $('#spinner').css('display', 'none');
        $('#url-input').val('');
        $('[name="tag-check"]').prop('checked', false);
        t.setState({tagData: []});
        apiURL = 'post';
      })
      .fail(function(xhr, status, err) {
        $('#spinner').css('display', 'none');
        alert(err);
      });
  },

これで終わり~(∩´∀`)∩ワーイ

( ゚д゚)ハッ!

そういえば、タグのリストのほうも2秒ごとに取得するようになっていたんでした。

タグのリストを取得するAPIは「./api/tag」のひとつしかないので、URLが変わったら~というのはなく、ただ単に更新されたかどうかだけわかればいいですね。
booleanで十分です。
まず最初に、判定用の変数を用意しておきます。

var tagBool = false;

次にタグリストを読み込んでいるSiteHeader.jsxを修正します。

  componentDidMount: function() {
    this.loadArticleFromServer();
    setInterval(this.fetchTag, 2000);
  },

初回の読み込み時は問答無用でloadArticleFromServer()します。
このあと、2秒ごとにtagBoolがtrueかfalseかを見て、trueだったらAPIを叩く、という処理をします。

  fetchTag: function() {
    if(tagBool){
      tagBool = false;
      this.loadArticleFromServer();
    }
  },

tagBoolはfalseに戻しておきましょうね。

あとは、URLを送信したときにtagBoolをtrueに変えるようにしておきます。

    $.ajax({
      url: './api/article',
      type: 'POST',
      data: {"url": inputURL, "tag": tagArr},
      timeout:10000,
      })
      .done(function() {
        $('#spinner').css('display', 'none');
        $('#url-input').val('');
        $('[name="tag-check"]').prop('checked', false);
        t.setState({tagData: []});
        apiURL = 'post';
        tagBool = true;
      })
      .fail(function(xhr, status, err) {
        $('#spinner').css('display', 'none');
        alert(err);
      });

さっき、apiURLを「post」にしたりしたのと同じところですね。

これで( `д´)b オッケー!

Socket.ioだとか使ってやるのもいいですが、この方が通信量は抑えられるかな…?
ユーザーの操作によってしかサーバにアクセスしないはず。

今回比較用に変数を定義したので、たとえば「新着あるよ~」プッシュ通知をユーザーがクリックしたら変数に「post」とか「true」とかを設定してやれば、同じ動きをして画面の内容を更新することが可能になりますね。

React使ってSPAを作るよ目次