RoR to React.js migration

Michał Koźmiński

kmichal.com

@michalkozminski

Problem

UX

User Experience

 

Current situation

Scalability

EC2 instance boots in 5-6 min

Migration

Problems

  • small team
  • maintaince of 2 versions
  • logic migration

Lets start

Divide and conquer

gem install react-rails

Benefits

  • Server side rendering
  • JSX with Babel support
  • Works with every template system
  • CoffeeScript support 

Split pages to react components

 

Title Text

Title Text

{ prerender: true }

  • Gives server side rendering
  • dosen't change current behaviour
  • We migrated templatest to react
const RecipeDetails = React.createClass({
  render() {
      return (
        <div className="row">
          <div className="abswrapper--container">
            <div className="container--recipe-images">
              <div className="slider__image">
                <img style={{width: '100%'}} src={this.props.recipe.images[0].url}/>
              </div>
            </div>
          </div>

          <div className="container--pin">
            <div className="container__header no-curves">
              <div className={`container__pin container__pin--${this.props.recipe.type.toLowerCase()}`} />
            </div>
            <div className="container__content medium-11 medium-centered fixed-buttons-gap" onClick={this.handleCTAButtonClick}>
              <div className="text-center bold-headline">
                <strong>{this.props.recipe.name}</strong>
              </div>

              <div className="row collapse">
                <div className="small-12 half-gap text-center std-text">
                  <div className="icon icon--timer">
                    {this.props.recipe.cooking_time / 60} min.
                  </div>
                  <span className="vertical-bars"></span>
                  <div className="icon icon--person">
                    from {this.props.recipe.servings} person
                  </div>
                </div>
              </div>

              <div className="row text-center">
                <div className="small-12 half-gap columns">
                  <ul className="tags">
                    {this.props.recipe.tags.map((recipeTag, index) => {
                      return(
                        <li key={index}>
                          <span className="tag">{recipeTag}</span>
                        </li>
                      );
                    })}
                  </ul>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    )
  }
});

Example component

Detaching from rails

Webpack
Hot loader
ES7

var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: [
    'webpack-dev-server/client?http://localhost:3001',
    'webpack/hot/dev-server',
    './webpackassets/index'
  ],
  devtool: 'cheap-eval-source-map',
  output: {
    path: __dirname + '/public/assets/',
    filename: 'index.js',
    publicPath: '/webpackassets/'
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoErrorsPlugin(),
    new ExtractTextPlugin('index.css')
  ],
  externals: [
    'google'
  ],
  resolve: {
    extensions: [ '.js', '.jsx', '.scss' ]
  },
  module: {
    loaders: [{
      test: /\.jsx?$/,
      loaders: [
        'flowcheck',
        'imports',
        'react-hot',
        'babel?stage=1&optional[]=runtime'
      ],
      exclude: /(node_modules|bower_components)/,
      include: path.join(__dirname, 'webpackassets')
    },
    {
      test: /\.scss$/,
      loader: "style!css!sass"
    }]
  },
  devServer: {
    contentBase: '/public',
    hot: true,
    port: 3001,
    proxy: {
      '*': 'http://127.0.0.1:3000'
    }
  }
};

webpack.config.js

app/assets/javascript/components

to

webpack/components

New problem

Components are not rendered anymore

Moving routing to frontend

 

react-router

import React from 'react'
import { Router, Route, HistoryLocation, DefaultRoute, create } from 'react-router'

import App              from './pages/app'
import RecipeDetails    from './pages/recipe_details'

let routes = (
  <Route name='root' handler={App}>
    <Route handler={RecipeDetails} path="/recipes/:recipe_id" name="recipe-details" />
  </Route>
)

export default create({ routes, HistoryLocation})
document.addEventListener("DOMContentLoaded", (event) => {
  Router.run((Handler, state) => {
    React.render(<Handler{...state} /> , document.getElementById('react-body'))
  })
})

Init react (index.jsx)

class ReactController < ApplicationController
  def render_react
  end
end

Create React endpoint

web: rails s
proxy: webpack-dev-server

----
foreman start

Procfile – lets start

State of App

React is for View

  • ​We don't have support for data fetch
  • No models
  • No routing
  • No controller

Easy way

  • Passing state from rails by globals
  • we keep site working
  • more or less hacky solution
class RecipeSerializer < ActiveModel::Serializer
  attributes :name, :description, :type, :price, :people
  has_many :ingredients, :images
end
const RecipeDetails = React.createClass({
  getInitialState() {
    return window.reactProxy.recipe; //get data from Serializer
  },
  render() {
    if(this.state.loading) {
      return (
        <div className="text-center double-gap">
          <i className="fa fa-circle-o-notch fa-spin fa-5x" />
        </div>
      );
    } else {
      return (
        <div className="row">
          <div className="abswrapper--container">
            <div className="container--recipe-images">
              <div className="slider__image">
                <img style={{width: '100%'}} src={this.state.recipe.images[0].url}/>
              </div>
            </div>
          </div>

          <div className="container--pin">
            <div className="container__header no-curves">
              <div className={`container__pin container__pin--${this.state.recipe.type.toLowerCase()}`} />
            </div>
            <div className="container__content medium-11 medium-centered fixed-buttons-gap" onClick={this.handleCTAButtonClick}>
              <div className="text-center bold-headline">
                <strong>{this.state.recipe.name}</strong>
              </div>

              <div className="row collapse">
                <div className="small-12 half-gap text-center std-text">
                  <div className="icon icon--timer">
                    {this.state.recipe.cooking_time / 60} min.
                  </div>
                  <span className="vertical-bars"></span>
                  <div className="icon icon--person">
                    from {this.state.recipe.servings} person
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      )
    }
  }
});

       

API

Fluxxor
Store
Action

Flux architecture

  • one way data flow
  • User interaction calls action
  • Dispatcher sends action to store
  • Store updates state
  • Store notifies View 

Action

import Request   from "superagent";
import Constants from "../constants";

export default {
     getRecipe(recipeId) {
      Request
        .get(`/api/recipes/${recipeId}`)
        .end((error, response) => {
          this.dispatch(Constants.RECIPES.LOAD_SUCCESS, response.body);
        });
    }
}

Store

import Request   from "superagent";
import Constants from "../constants";

const Store = Fluxxor.createStore({
  initialize() {
    this.recipe = {};

    this.bindActions(
      Constants.RECIPES.LOAD_SUCCESS, this.onRecipeLoadSuccess,
    );
  },
  
  onRecipeLoadSuccess(payload) {
    this.recipe = payload;
    this.emit("change");
  },
  
  getState(){
    return {recipe};
  }
});

export default Store;

View

import React                            from "react";
import { FluxMixin, StoreWatchMixin }   from "fluxxor";

const RecipeDetails = React.createClass({
  mixins: [FluxMixin(React), StoreWatchMixin("Store")],

  getStateFromFlux() {
    return (this.getFlux().store("Store").getState());
  },
  render() {
    ....
  }
});

Translations

yaml2json and react-intl

import { IntlMixin, FormattedMessage }  from 'react-intl';

const RecipeDetails = React.createClass({
  mixins: [IntlMixin...],

  render() {
    return (
       ...
       <div className="row">
          <h4 className="gap"><FormattedMessage message={this.getIntlMessage('details_page.title')} /></h4>
        </div>
       ...
    )
  }
});

Deployment

Helper

module ApplicationHelper
  def javascript_bundle_url
    version_path = Rails.root.join('public/version.json')

    if Rails.env.production?
      version = JSON.parse(File.read(version_path))["version"]
      "/webpackassets/index.#{version}.js"
    else
      "/webpackassets/index.js"
    end
  end
end

Layout

...
%script{src: javascript_bundle_url, type: 'text/javascript'}
...

webpack.config

  plugins: [
    function () {
      this.plugin("done", function(stats) {
        require("fs").writeFileSync(
          path.join(__dirname, "public", "version.json"),
          JSON.stringify({ version: stats["hash"] }));
      });
    }
  ],

Thanks, Bye

slides online at: kmichal.com

deck

By Michał Koźmiński