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
deck
- 804