JSXとは、JavaScriptにHTML風の構文を組み込んだ表現方法である。JSとは別物で、トランスパイラでJSに変換しないと使えない。また、React.jsとは独立して使えるらしい。

曖昧な理解で適当に使うと、[object Object]みたいにうまく表示されない時に困る。というか困っているので、JSXとは何か、というところから基本的な知識をまとめたいと思う。

JSXとは

JSXの仕様のドラフトWikipediaでは、「JSXとはXML風の拡張構文で、JavaScript XMLの略だ」みたいなことが書かれている。

ただ、Reactの最新のドキュメントでは、以下のような表現が使われている。

JSX lets you write HTML-like markup inside a JavaScript file, keeping rendering logic and content in the same place. Sometimes you will want to add a little JavaScript logic or reference a dynamic property inside that markup.

引用元:Writing Markup with JSX – React

訳:JSXは、レンダリングロジックとコンテンツを同じ場所に保持し、JavaScriptファイルの中でHTMLライクなマークアップが書けるようにする。そのマークアップの中にJavaScriptのちょっとしたロジックや動的プロパティへの参照も追加できる。

DOMの構造をマークアップする独自の言語という意味では「XML-like」の方が正確である。しかし実際の使用シーンを鑑みると「HTML-like」の方が直感的に伝わる。このために、あえて「HTML-like」に変えたのではないかと思う。

また、JSXはJavaScriptファイルの中に書けるが、ECMAScriptの標準仕様ではない。そのままでは機能せず、BabelやTypeScriptコンパイラなどを使って、標準仕様を満たすJavaScriptにトランスパイルする必要がある。

ツールにとっても開発者にとっても、それがただのJavaScriptファイルか、JSXを含んだJavaScriptファイルかを区別する方法があると便利だ。そのためJSXが含まれたファイルには、.jsxという拡張子が使われるのが一般的である。

特徴

既存の言語に自前の新しい構文を埋め込むのは厄介である。構文拡張を考えるのが自分ひとりとは限らないし、言語自体が互換性のない構文拡張を行う可能性もある。JavaScriptのような人気言語ではなおさらだ。

新しい構文拡張と共存するために、JSXはシンプルで、ミニマルで、親しみやすい、独立した構文を持っている。

ちなみに、この仕様はXMLやHTMLの仕様に準拠しようとはしていない。JSXはECMAScriptの機能として設計されている。XMLに寄せているのは、親しみやすさのためだけであるらしい。

Reactとの関係

JSXは、もともとReactの一部として開発された。ただ、JSXとReactはそれぞれ独立している。JSXは構文拡張であり、ReactはJavaScriptライブラリである。

React 16までは、JSXをJavaScriptにトランスパイルする際にReactをインポートする必要があった。これは、HTMLのレンダリングにReact.createElement()メソッドが使われていたためである。

React 17でJSXの置換方法が改善され、React.createElement()jsx()jsxs()に分かれた。前者が動的なコンポーネント、後者が静的なコンポーネントの変換を担当する(jsxsの’s’はstaticの’s’)。

これによってReactをインポートする必要がなくなり、構成がちょっとすっきりした。

とはいえJSXをJavaScriptに変換するための関数群(jsx-runtime)はReactのパッケージに含まれており、コンポーネントやフックなどもReactがなければ使えない。

独自にJSXランタイムを開発することは可能であるし、そうしたツールも1つ2つではない(Vue,Preact,TypeScriptコンパイラなど)。ただ、依然としてReactとJSXは切っても切れない関係にあると言える。

JSXの基本的なルール

JSXの基本的なルールをまとめる。

  • JSX要素は、必ず単一の親要素でラップされる。
  • 不要な要素を使いたくないときは、フラグメント(<></>)でラップする。
  • 閉じタグを忘れない。単一のタグも<tagname />みたいに閉じる。
  • 属性名は、ハイフンを使わず、キャメルケースで書く。
  • classは、classNameforは、htmlForと書く。
  • aria-*属性とdata-*属性はHTMLと同じように書く。
  • 属性値に文字列を使うときは、'"で囲う。
  • 属性値とタグの中身のテキストは、動的に指定できる。
  • 動的な指定は{ 式 }の形で行う。{}の間では、JavaScriptの式がすべて使える。
  • {}の中にJSX要素の配列変数を置くと、自動で順番にレンダリングされる。
  • スプレッド構文は、属性値でしか使えない。
  • style属性には、文字列を渡せない。ので、オブジェクトを渡す。
  • {{key:value}}のように括弧を二重にすると、オブジェクトを渡せる。
  • 整形などでJXSを改行する際は、()で囲うとセミコロンの自動挿入を避けられる。
  • JSX内でコメントを書く際には {/* コメント内容 */}{// コメント}の形式を使う。

捉え方のコツ

箇条書きにするとワサワサしているように見える。しかし、JSXはシンプルな概念である。

裸のHTMLは、JavaScriptでは使えない。document.createElement()メソッドを使うか、文字列を変換するかしてDOMオブジェクトを作り操作するしかない。

文字列だと簡単に構造を表現できるが、動的な操作が難しい。一方、DOM APIでは動的な操作はできるが、構造を表現するのが難しい。しかも命令を長々と書き連ねなければならない。

JavaScriptの中でHTMLを書き、最後にHTMLの部分をJavaScriptオブジェクトに変換すれば、この問題は解決する。HTMLで動的に変更される可能性がある箇所、つまり属性値とテキストコンテンツにJavaScriptの値を埋め込めるようにすれば、なお使い勝手が良い。

jsx
function somethingList(listContents = ["零", "壱", "弐"]) {
  return (
    <ul>
      {listContents.map((c, i) => (
        <li id={i}>{c}</li>
      ))}
    </ul>
  );
}

これがJSXである。

コンポーネントやフックはReactの概念で、JSXとは関係ない。JSXは、JavaScriptのコード中に直接書かれた裸の<タグ名 属性名={変数名1}>{変数名2}</タグ名>みたいな部分である。

私が出会した[object Object]みたいな表示は、JSXをテンプレート文字列とごっちゃにして扱ったせいだった。そのせいで、JSX要素ではなく、ただのJavaScriptオブジェクトをレンダリングしようとしていた。

多くの場合、JSXでは文字列としてHTMLコードを扱う必要がない。<タグ名 属性名={変数名1}>{変数名2}</タグ名>のように、裸のHTMLを書けば足りるからだ。

JSXをJavaScriptに変換する方法

JSXをただJavaScriptに変換するだけであれば、以下の手順で終わる。

Babelでトランスパイルする

BabelでJSXをJavaScriptにトランスパイルするには、@babel/plugin-transform-react-jsxを使う。

bash
npm i -D @babel/cli @babel/core @babel/plugin-transform-react-jsx

続いてBabelの設定ファイル(babel.config.json)を用意する。@babel/plugin-transform-react-jsxには、以下のようなオプションがある。

プロパティ名 概要
throwIfNamespace 真偽値。XMLの名前空間タグが使われた場合に、エラーを投げるか。デフォルトはtrue
runtime automatic or classic。自動でインポート文を挿入するか、しないか。デフォルトはclassic(しない)。
importSource 文字列。インポート文のインポート元ディレクトリ。デフォルトはreact
pragma 文字列。変換に使う関数名を指定する。runtimeclassicの場合に有効。デフォZルトはReact.createElement()
pragmaFrag 文字列。フラグメントの変換に使う関数名を指定する。runtimeclassicの場合に有効。デフォルトはReact.Fragment()
useBuiltIns 真偽値。JSX中でスプレッド構文が使われている時に、プロパティのアサインにObject.assignを使うか、Babelのヘルパー関数を使うか。デフォルトはfalse
useSpread 真偽値。JSX中でスプレッド構文が使われている時に、プロパティのアサインにスプレッド構文を使うか、Babelのヘルパー関数を使うか。デフォルトはfalseuseBuiltInstrueの場合でも、こちらが優先される。

参考:@babel/plugin-transform-react-jsx · Babel

今回は一番楽な設定を選ぶ。

json
{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "runtime": "automatic"
      }
    ]
  ]
}

あとは、適当なJSXファイルを用意するのみである。ここでは以下の内容をscript.jsxに保存する。

jsx
function somethingList(listContents = ["零", "壱", "弐"]) {
  return (
    <>
      <h2>なにかのリスト</h2>
      <ul>
        {listContents.map((c, i) => (
          <li id={i}>{c}</li>
        ))}
      </ul>
    </>
  );
}

以下のBabelコマンドでトランスパイルする。

bash
npx babel script.jsx -o compiled-script.js

出力ファイルはcompiled-script.jsとした。出力内容は、以下のようになる。

js
import { jsx as _jsx } from "react/jsx-runtime";
import { Fragment as _Fragment } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
function somethingList(listContents = ["零", "壱", "弐"]) {
  return /*#__PURE__*/ _jsxs(_Fragment, {
    children: [
      /*#__PURE__*/ _jsx("h2", {
        children: "\u306A\u306B\u304B\u306E\u30EA\u30B9\u30C8",
      }),
      /*#__PURE__*/ _jsx("ul", {
        children: listContents.map((c, i) =>
          /*#__PURE__*/ _jsx("li", {
            id: i,
            children: c,
          })
        ),
      }),
    ],
  });
}

JSXランタイムのimport文は、その環境にインストールされているかどうかに関係なく、設定に基づいて自動で挿入される(runtimeオプションがclassicの場合は手動で書く)。

そのため、上記のコードを実際に使える形にするには、react/jsx-runtime(とその依存関係)を用意した上でバンドルする必要がある。

これは、他のJSXランタイムを利用する場合も同様である。

メモ

If you use JSX with a library other than React, you can use the importSource option to import from that library instead — as long as it provides the necessary entry points. Alternatively, you can keep using the classic transform which will continue to be supported.

If you’re a library author and you are implementing the /jsx-runtime entry point for your library, keep in mind that there is a case in which even the new transform has to fall back to createElement for backwards compatibility. In that case, it will auto-import createElement directly from the root entry point specified by importSource.

引用元:Introducing the New JSX Transform – React Blog

参考資料