動かざることバグの如し

近づきたいよ 君の理想に

RailsからJSにデータを渡す時は script が便利

やりたいこと

Railsでビューを作っていると、コントローラーで用意したデータをJavaScript側でも使いたい場面がわりとある。

<script type="application/json"> タグにJSONとして埋め込む方法を紹介する。DjangoやNext.jsでも使われているパターンだが、素のRailsには専用ヘルパーがないので自前で実装する必要がある。

コード

まずRails側でデータを用意する。

class HomeController < ApplicationController
  def index
    @products = [
      { id: 1, name: "りんご", price: 120, category: "果物", in_stock: true },
      { id: 2, name: "バナナ", price: 80, category: "果物", in_stock: true },
      { id: 3, name: "にんじん", price: 60, category: "野菜", in_stock: false },
      { id: 4, name: "トマト", price: 150, category: "野菜", in_stock: true },
      { id: 5, name: "ぶどう", price: 300, category: "果物", in_stock: false }
    ]
  end
end

で、erb側は <script type="application/json"> でデータを渡す。

<script type="application/json" id="products-data">
  <%= @products.to_json.html_safe %>
</script>

これをJS側でデータロードする。 やってることは究極的には JSON.parse(el.textContent)

type Product = {
  id: number;
  name: string;
  price: number;
  category: string;
  in_stock: boolean;
};

/**
 * script[type="application/json"] に埋め込まれたJSONをパースして返す。
 * XSSを避けるため innerHTML ではなく textContent で取得する。
 */
function readJsonScript<T>(elementId: string): T | null {
  const el = document.getElementById(elementId);
  if (!el || el.getAttribute('type') !== 'application/json') return null;
  return JSON.parse(el.textContent ?? 'null') as T;
}

const products = readJsonScript<Product[]>('products-data');

console.log(products);

同じ目的を達成できる data-* 属性と比較する。

data-* を使うと次のように書ける。

<div id="app" data-products="<%= @products.to_json %>"></div>
const el = document.getElementById('app')
const products = JSON.parse(el.dataset.products)

2つのアプローチを比較するとこうなる。

script[type=json] data-*
データ量 大量OKサイズ制限なし 大量データは重くなる(HTML属性のエスケープ処理が走る)
配列・ネストOK 文字列として埋め込むので毎回JSON.parseが必要
紐付け IDで参照(要素と別) 要素に直接紐付く
可読性 HTMLとデータが分離してすっきり 属性が長くなり見づらい
エスケープ .to_json.html_safeでOK html_escapeが二重にかかる場合がある

data-* が向いているのは要素自体に関するシンプルな値(1〜2個のプリミティブ)や、StimulusなどのJSフレームワークと data-controller / data-value を組み合わせる場面だ。

<button data-user-id="42" data-confirm-message="削除しますか?">削除</button>

参考リンク