ReactとRuby on Railsを連携させる時によくやる方法
既存のRails製プロダクトのフロントエンドを徐々にReactベースに置き換えていったり、一部のUIコンポーネントだけReactで動かしたりする際に使っているヘルパーをメモしておきます。
Rails側にView用のヘルパーを設置する。
# app/helpers/react_helper.rb module ReactHelper # Reactコンポーネントを表示します # props: Propsに渡すJSONエンコード可能なデータ def render_component(component, props = {}) raw render(partial: 'helpers/react/component', locals: { component: component, props: props }) end end
/ app/views/helpers/react/_component.html.slim div.react-component[ data-component=(component) data-props=(props.to_json) ]
次に、webpackerのエントリーポイント配下で以下のスクリプトを読み込ませる。
// app/javascript/react-bridge.tsx import React from "react" import ReactDOM from "react-dom" const selector = ".react-component" /** RailsのviewからReactComponentを探して描画 */ const mountComponents = () => { document.querySelectorAll<HTMLDivElement>(selector).forEach(async host => { try { const componentName = host.dataset["component"] const Component: React.ComponentType<any> = _componentStore.get( componentName ) assert( Component, `bridge.react: component not defined with name: ${componentName}` ) if (!Component) return let props: object | undefined = undefined try { const rawProps = host.dataset["props"] props = rawProps && JSON.parse(rawProps) } catch (e) {} let options: MountOption | undefined = undefined try { const rawOptions = host.dataset["options"] options = rawOptions && JSON.parse(rawOptions) } catch (e) {} render( <ComponentWrapper> <Component {...props} /> </ComponentWrapper>, host ) } catch (e) { assert(false, e) } }) } /** ページ遷移時に古いコンポーネントをGC */ const cleanupComponents = () => { const nodes = document.querySelector(selector) nodes && ReactDOM.unmountComponentAtNode(nodes) } const ComponentWrapper: React.FC<{ noTheme?: boolean }> = props => { // コンテキスト等の挿入はここで return <>{props.children}</> } /** コンポーネントの読み込み機構を初期化して監視を開始 */ export const start = () => { document.addEventListener("turbolinks:load", mountComponents) document.addEventListener("turbolinks:visit", cleanupComponents) } /** コンポーネントをローダーに追加 */ export const registerComponent = (component: any, name?: string) => { name = name || component?.displayName || component?.constructor?.name _componentStore.set(name, component) } function assert(value: any, message?: string | Error): asserts value { if (!value) { if (process.env.NODE_ENV === "production") { if (message) { console.log(message) } } else { throw message || "assertion error" } } } const _componentStore = new Map<string, any>()
次にapplication.jsに以下を挿入
// app/javascript/packs/application.ts require("../react-bridge").start()
コンポーネントをViewから呼び出せるように登録する。(application.js等で実行)
const SomeComponent = () => (<div>Hello</div>) registerComponent(SomeComponent, 'SomeComponent');
RailsのViewから以下の様に呼び出す。
= render_component 'SomeComponent', { foo: 'bar' }