もふもふ技術部

IT技術系mofmofメディア

既存Railsアプリに後からReactとTypeScriptを導入する

今回は、弊社メンバーが研修時に実装した課題のアウトプットがちょうど良かったので、そこに後からReactとTypeScriptを導入していきます。

この課題は、万葉さんが公開しているものを使っていて、シンプルなTODO管理アプリケーションです。

https://github.com/everyleaf/el-training

既に実装済みのものに乗せていくので、タスク一覧・詳細・編集画面があります。

タスク一覧

タスク詳細

タスク編集

react-rails gemを使う方法

react-railsRailsで実装された、erbやslimなどのビューの中で、Reactコンポーネントを扱えるようにするgemです。

いわゆる、フロントとバックエンドを分離しSPAにするものではなく、画面の一部をリッチにするような用途で使用します。

このあたりの記事を参考にやってみます。

https://qiita.com/sakakinn/items/e9cc99873bd5508c29ed

Gemfile

gem 'react-rails'
$ bundle install
$ rails webpacker:install:react

react:installすると何やら生成される。

$ rails generate react:install

...

warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
      append  app/javascript/packs/application.js
      create  app/javascript/packs/server_rendering.js

コンポーネントを生成してみる。コンポーネント名(HelloWorld)とProps(greeting:string)を指定する。

$ rails g react:component HelloWorld greeting:string

こんな感じのが生成される

app/javascript/components/HelloWorld.js

import React from "react"
import PropTypes from "prop-types"
class HelloWorld extends React.Component {
  render () {
    return (
      <React.Fragment>
        Greeting: {this.props.greeting}
      </React.Fragment>
    );
  }
}

HelloWorld.propTypes = {
  greeting: PropTypes.string
};
export default HelloWorld

適当なViewでコンポーネントを呼び出してみる。今回はタスク一覧画面で呼び出した。

app/views/tasks/index.html.slim

...

= react_component("HelloWorld", { greeting: "Hello from react-rails." })

無事表示されました。

タスク一覧

react-rails gemを使わない方法

上の実装の場合、基本的にサーバとの通信はRailsを介さなければならないので、Railsとガッツリ結合している状態です。

フロントとバックエンドを分離することを見越して、react_componentを使わずフロント側だけでReactコンポーネントを呼び出す実装に変えてみます。

application.js

// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

// Uncomment to copy all static images under ../images to the output folder and reference
// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
// or the `imagePath` JavaScript helper below.
//
// const images = require.context('../images', true)
// const imagePath = (name) => images(name, true)
// Support component names relative to this directory:
var componentRequireContext = require.context("components", true);
var ReactRailsUJS = require("react_ujs");
ReactRailsUJS.useContext(componentRequireContext);

import App from '../components/HelloWorld'
import React from 'react'
import ReactDOM from 'react-dom'

document.addEventListener('DOMContentLoaded', () => {
    const ele = document.getElementById('app')

    ReactDOM.render(
        <App greeting="Hello from react" />,
        ele
    )
})

application.html.slim

doctype html
html
  head
    title
      | ElTraining
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
  body
    = render 'shared/flash'
    = render 'shared/header'
    #app
    = yield

ちゃんと表示されました!

タスク一覧

TypeScriptを導入する

コマンドでインストールします。

$ rails webpacker:install:typescript

HelloWorld.tsxに名前を変更し、ソースコードも修正します。

import React from "react"

interface Props {
  greeting: string
}

class HelloWorld extends React.Component<Props> {

  render () {
    return (
      <React.Fragment>
        Greeting: {this.props.greeting}
      </React.Fragment>
    );
  }
}

export default HelloWorld

起動すると怒られる。

Failed to compile.

/Users/atsushiharada/source/el-training/app/javascript/components/HelloWorld.tsx
./app/javascript/components/HelloWorld.tsx 1:7-12
[tsl] ERROR in /Users/atsushiharada/source/el-training/app/javascript/components/HelloWorld.tsx(1,8)
      TS1259: Module '"/Users/atsushiharada/source/el-training/node_modules/@types/react/index"' can only be default-imported using the 'allowSyntheticDefaultImports' flag

tsconfig.jsonに追記する。

{
  "compilerOptions": {
    "esModuleInterop": true
  }
}

できたああああああ!!

タスク一覧

一応このソースコードリポジトリも載せておきます。少し中途半端状態になっているので、上記のソース通りになっていないかもですがご容赦を。

https://github.com/harada4atsushi/el-training