ちょっと話題の記事

RailsをバックエンドにしたFlux(React + Alt)を試してみる

2015.07.23

丹内です。

最近ReactJSを触り始めました。

掲題の通り、Railsをバックエンドにした場合のFluxを書いてみたのでまとめます。

このエントリの参考URLにあるリポジトリを参考にしました。

バックエンド

普通のRailsアプリです。Scaffoldをベースに、フロントエンドを書いていきます。

db/migtate/create_books.rb

class CreateBooks < ActiveRecord::Migration
  def change
    create_table :books do |t|
      t.string :title
      t.timestamps null: false
    end
  end
end

BooksController#index

今回、ブラウザで/booksにアクセスした時に、最初に空っぽのHTMLを返し、ReactJSでAjaxリクエストによってデータを取得しDOMを構築する方法をとったため、indexアクションにJSONの場合を追加しています。

def index
  @books = Book.all
  respond_to do |format|
    format.html { render :index }
    format.json { render json: @books }
  end
end

実装

rails newでアプリを生成したあと、Railsアプリ直下にclientという名前でディレクトリを作り、そこで適当にクライアントサイド開発環境を作ります。

app/books/index.html.slim

HTML(テンプレートエンジンはSlimです)は、これだけです。

h1 Listing books
#content

ここからAjaxで取得したデータをDOMとしてくっつけます。

client/assets/javascripts/App.jsx

ここがクライアントサイドのエントリポイントとなります。つまり、ブラウザでhttp://localhost:3000/booksにアクセスしてページを読み込んだ直後にここから処理が始まります。

import $ from 'jquery';
import React from 'react';
import BookBox from './components/BookBox';

$(function onLoad() {
    function render() {
        React.render(
            <div>
                <BookBox url='books.json'/>
            </div>,
            document.getElementById('content')
        );
    }
    render();
});

このBookBoxは別のReactClassとして定義してあり、それをimportして呼び出すだけの処理です。

client/assets/javascripts/components/BookBox.jsx

ReactJSの記述が本格的に始まります。まずは諸々のライフサイクルを定義しています。

import React from 'react';
import BookStore from '../stores/BookStore';
import BookActions from '../actions/BookActions';
import BookList from '../components/BookList';

const BookBox = React.createClass({
    displayName: 'BookBox',

    propTypes: {
        url: React.PropTypes.string.isRequired
    },

    getStoreState() {
        return {
            books: BookStore.getState()
        };
    },

    getInitialState() {
        return this.getStoreState();
    },

    componentDidMount() {
        BookStore.listen(this.onChange);
        BookActions.fetchBooks(this.props.url, true);
    },

    componentWillUnmount() {
        BookStore.unlisten(this.onChange);
    },

    onChange() {
        this.setState(this.getStoreState());
    },

    render() {
        return (
            <div className="bookBox container">
                <BookList books={ this.state.books.books }/>
            </div>
        );
    }
});

export default BookBox;

conponentDidMount()で呼び出したBookStore.fetchBooks()でAjaxリクエストを実行し、renderでその結果を参照しています。

client/assets/javascripts/actions/BookActions.js

ここからはReactJSではなくAltの領域になります。まずはActionです。
fetchBooksでAjaxリクエストを送り、コールバックでこのクラスのupdateBooksを呼んでいます。
少し分かりにくいのですが、その中のthis.dispatchでStoreと連携させています。

import alt from '../FluxAlt';
import $ from 'jquery';

class BookActions {
    fetchBooks(url) {
        $.ajax({
            url: url,
            dataType: 'json'
        }).then(
            (books) => this.actions.updateBooks(books),
            (errorMessage) => this.actions.updateBooksError(errorMessage)
        );
    }

    updateBooks(books) {
        this.dispatch(books);
    }

    updateBooksError(errorMessage) {
        this.dispatch(errorMessage);
    }
}

export default alt.createActions(BookActions);

ここでimportしているFluxAltは、以下のようなユーティリティです。(client/assets/javascripts/FluxAlt.js)

import Alt from 'alt';
const alt = new Alt();

export default alt; 

client/assets/javascripts/stores/BookStore.js

ここもAltです。Fluxで言うところのStoreです。
ポイントはconstructor内のthis.bindListenersで、ここでActionからdispatchした関数とStore内の関数を結びつけています。

import alt from '../FluxAlt';
import BookActions from '../actions/BookActions';

class BookStore {
    constructor() {
        this.books = [];
        this.errorMessage = null;
        this.bindListeners({
            handleFetchBooks: BookActions.FETCH_BOOKS,
            handleUpdateBooks: BookActions.UPDATE_BOOKS,
            handleUpdateBooksError: BookActions.UPDATE_BOOKS_ERROR
        });
    }

    handleFetchBooks() {
        return false;
    }

    handleUpdateBooks(books) {
        this.books = books;
        this.errorMessage = null;
    }

    handleUpdateBooksError(errorMessage) {
        this.errorMessage = errorMessage;
    }
}

export default alt.createStore(BookStore, 'BookStore');

その他コンポーネント

以下、client/assets/javascripts/components/BookBox.jsxから読んでいるComponentです。
まずはclient/assets/javascripts/components/BookList.jsx

import React from 'react';
import Book from './Book';

const BookList = React.createClass({
    displayName: 'BookList',

    propTypes: {
        books: React.PropTypes.array
    },

    render() {
        const data = this.props.books;
        const bookNodes = data.map((book, index) => {
            return(
                <Book title={book.title} key={index}/>
            );
        });

        return (
            <div className="bookList">
                { bookNodes }
            </div>
        )
    }
});

export default BookList;

そしてBookListの中で読んでいるclient/assets/javascripts/components/Book.jsx

import React from 'react';

const Book = React.createClass({
    displayName: 'Book',

    propTypes: {
        title: React.PropTypes.string.isRequired
    },

    render() {
        return(
            <ul>{this.props.title}</ul>
        );
    }
});

export default Book;

まとめ

RailsアプリにAjaxリクエストを送るFluxを、Altを使って実装してみました。
クライアントサイドにはまだ入門したてなのですが、Node.JSを使ったフロントエンド環境構築だけで数日かかってしまいました。
RailsのAsset Pipelineにいい感じに載せられて開発効率を上げられるようにアセット管理をしつつ、もっとフロントエンド開発をして行きたいです。

参考URL