Reactを用いることで、アプリケーションの状態によらず、宣言的にUIを構築することができます。 Fluxフレームワークを用いることで、設計を構造化でき、よりメンテナブルに実装できます。 この記事では、Rails、Fluxフレームワークの1つであるRedux、Reactでアプリケーションを開発する手順について解説します。

動作環境

この記事では以下の環境で動作を確認しています。

  • Ruby on Rails: 4.2.6
  • Redux: 3.5.2
  • React: 15.0.2

手順

1. アプリケーションを生成する

まず、アプリケーションの基礎を生成します。 以下により、Railsアプリケーションであるsampleディレクトリと、このディレクトリ直下にNode.jsの依存関係などを定義するpackage.jsonが生成されます。

$ rails new sample
$ cd sample
$ npm init

2. Gemをインストールする

使用するGemをGemfileに追記し、bundle installします。

gem 'browserify-rails'
gem 'react-rails'

3. JavaScriptの各ファイルを更新する

react-railsのジェネレータにより、JavaScriptの設定を更新します。

$ bin/rails g react:install

また、上記コマンドで更新されたapplication.jsを編集します。 最低限以下の2つがあればよいです。

app/assets/javascripts/application.js:

//= require react_ujs
//= require components

4. ディレクトリを作成する

Reduxに従って実装した各ファイルを配置するディレクトリを作成します。

$ cd app/assets/javascripts/components
$ mkdir actions
$ mkdir components
$ mkdir containers
$ mkdir reducers
$ mkdir store

5. Browserifyの設定を行なう

browserify-railsのCLIオプションとしてBabelifyを設定します。

config/application.rb:

# Browserify transform for Babel
config.browserify_rails.commandline_options = '-t babelify'

6. パッケージをインストールする

Node.jsの各種パッケージをインストールします。 --save-devは開発環境のみで利用し、--saveは本番環境でも利用するパッケージです。

npm install --save-dev browserify
npm install --save-dev browserify-incremental
npm install --save-dev babelify
npm install --save-dev babel-preset-es2015
npm install --save-dev babel-preset-react
npm install --save react
npm install --save react-redux
npm install --save redux
npm install --save redux-thunk

7. Babelの設定を行なう

Babelはデフォルトではプラグインがない状態なので、.babelrcに使用するプラグインを定義します。

.babelrc:

{
  "presets": ["es2015", "react"]
}

以上でRails+Redux+Reactでの開発環境の構築は完了となります。

サンプル

以下に、ユーザ情報を閲覧するための簡単なサンプルを紹介します。 各ファイルの詳細は公式ドキュメントが参考になります。

app/.../components/actions/user.js:

export const SET_USER = 'SET_USER';

export function setUser(user) {
  return {
    type: SET_USER,
    user: user
  };
}

app/.../components/components/User.js:

import React, { Component } from 'react';

class User extends Component {
  render() {
    const { user } = this.props;

    return (
      <span>{user.name}</span>
    );
  }
}

export default User;

app/.../components/containers/Root.js:

import React, { Component } from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import UserApp from './UserApp';
import configureStore from '../store/configureStore';
import { setUser } from '../actions/user';

const store = configureStore();

export default class Root extends Component {
  componentWillMount() {
    store.dispatch(setUser(this.props.user));
  }

  render() {
    return (
      <Provider store={store}>
        <UserApp />
      </Provider>
    );
  }
}

app/.../components/containers/UserApp.js:

import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import User from '../components/User';
import * as UserActions from '../actions/user';

function mapStateToProps(state) {
  return {
    user: state.user
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(UserActions, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(User);

app/.../components/reducers/index.js:

import { combineReducers } from 'redux';
import user from './user';

const rootReducer = combineReducers({
  user
});

export default rootReducer;

app/.../components/reducers/user.js:

import { SET_USER } from '../actions/user';

const initialState = {
  name: 'guest'
};

export default function user(state = initialState, action) {
  switch (action.type) {
    case SET_USER:
      return action.user;
    default:
      return state;
  }
}

app/.../components/store/configureStore.js:

import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';

const createStoreWithMiddleware = applyMiddleware(
  thunk
)(createStore);

export default function configureStore(initialState) {
  return createStoreWithMiddleware(rootReducer, initialState);
}

app/.../javascripts/components.js:

window.React = require('react');
window.ReactDOM = require('react-dom');
window.Root = require('./components/containers/Root').default;

config/routes.rb:

Rails.application.routes.draw do
  resources :users, only: :show
end

app/controllers/users_controller.rb:

class UsersController < ApplicationController
  def show
    @user = { name: 'test' }
  end
end

app/views/users/show.html.erb:

<%= react_component('Root', { user: @user }) %>

以上で実装は完了です。 /users/1にアクセスし、「test」と表示されれば問題なく動作していることになります。

おわりに

今回は開発環境とサンプルの紹介を行ないました。 別の記事ではより実践的なアプリケーションの構築について書きたいと思います。