JSXをただのテンプレートエンジンとして書いてみる

6 min read読了の目安(約5900字

やったこと

  • サーバーサイドで、LINE Flex Messageを生成する必要があった
  • JSONでUIを表現するものだが、コンポーネントを作りたかった
  • 引数が来て、分岐や繰り返しを含む制御を経て、最終的にはJSON文字列を出力したい
  • tsconfigでJSXをコンパイルできるみたいな設定があるので、JSXで書いてみた

この記事は

  • Reactやpreactの実装を解説するものではないです。
  • 勉強がてらやってみたというだけの記事です。

環境

  • ts 4.2.3
  • その他、こちらを参照のこと

やりたいことの整理

  • JSON文字列がアウトプットのテンプレートを管理したい
  • 再利用したいので、最終的に文字列を返す関数をコンポーネントとして管理したい

コンパイラの挙動の確認

(()=> {
const React = {}

const List = (props: {item:string[]})=> {
  return <>
  start
  {
    props.item
  }
  end
  </>
}

const Sample = () => {
  return <List item={['a','b']}/>
}

console.log(Sample())
})()

上記のソースは、以下のようにトランスパイルされる。

"use strict";
(() => {
    const React = {};
    const List = (props) => {
        return React.createElement(React.Fragment, null,
            "start",
            props.item,
            "end");
    };
    const Sample = () => {
        return React.createElement(List, { item: ['a', 'b'] });
    };
    console.log(Sample());
})();

ここからわかることは、

  • <なにか props="aa">{children}</なにか> は、React.createElement(なにか,props,...children) の形式になる。

ということである。

というか、それぐらいのことは普通に公式ドキュメントに書いてあるが、実際にそういう挙動であることを確認するのはいいことだ。

React.createElementを実装する

シグネチャ

ということで、React.createElement の関数は以下のようなシグネチャを持つことになるだろう。

type FC<X> = ((props:PropsWithChildren<X>) => string) | string;
type PropsWithChildren<P> = P & { children?: ReactNode }
type ReactNode = string | string[];
type CreateElement = <X extends FC<A>,A>(f:X,props:A,...children?: ReactNode) => string;

今回はなんちゃってテンプレートエンジンが欲しいのであって、得たいのはstringの返り値なので関数コンポーネントの返り値もstringになる。

なので当然ReactNodeの型もstringまたはstring[]で問題ない。

実装

さて、CreateElementには「stringを返す関数、またはstring自身」と、「そのProps」と、「children」が渡ってくる。

したがって、普通にそれらを解決すればOKである。

(途中でめんどくなってany使っちゃいましたが、nodeがFC<A> | stringで、propsがA | nullということが言えるはずなのでいい感じに型を書くとさらにいいと思います)

const React = {
  createElement: <X extends FC<A>,A>(x:X,props:A,...children: ReactNode[]) => {
    return React.resolve({...props,children:children},x);
  },
  resolve: (props:any, node: Function | string) => {
    if(typeof node === 'string'){
      return node;
    }
    return node(props);
  }
}

こんな感じ。

React.Fragmentを実装する

要件

JSXでコンポーネントを書いていくと、どこかでReact.Fragmentに到達する。

今回の例でいうと、「List」コンポーネントがそれにあたる。

const List = (props: {item:string[]})=> {
  return <>
  start
  {
    props.item
  }
  end
  </>
}
const List = (props) => {
    return React.createElement(React.Fragment, null,
        "start",
        props.item,
        "end");
};

シグネチャ

当然だが、React.FC の仕様を踏襲するはずだ。

type Fragment = (props: PropsWithChildren<null>) => string;

今回の場合、FragmentはPropsを取らないため、純粋に渡ってきたchildrenを左から右に並べて表示すればよい。

const React = {
  createElement: <X extends FC<A>,A>(x:X,props:A,...children: ReactNode[]) => {
    return React.resolve({...props,children:children},x);
  },
  Fragment: (props: {children?:ReactNode[]}) => {
    return props.children?.flat().map(node=>React.resolve(null,node)).join('');
  },
  resolve: (props:any, node: Function | string) => {
    if(typeof node === 'string'){
      return node;
    }
    return node(props);
  }
}

こんな感じ。

最終形

生成されたコードは以下のようになる。
なおソースはこちら

"use strict";
(() => {
    const React = {
        createElement: (x, props, ...children) => {
            return React.resolve({ ...props, children: children }, x);
        },
        Fragment: (props) => {
            return props.children?.flat().map(node => React.resolve(null, node)).join('');
        },
        resolve: (props, node) => {
            if (typeof node === 'string') {
                return node;
            }
            return node(props);
        }
    };
    const List = (props) => {
        return React.createElement(React.Fragment, null,
            "start",
            props.item,
            "end");
    };
    const Sample = () => {
        return React.createElement(List, { item: ['a', 'b'] });
    };
    console.log(Sample());
})();

実行すると(どこでも実行できる)、コンソールには

startabend

と表示される。まあ、普通ですね・・・

まとめ

結果的に勉強になったのでよかった。(感想)

JSXは通常のテンプレートエンジンに比べて、

  • 複雑な独自制御構文を覚える必要が無い。
  • コンポーネントに切り出しやすく、かつコンポーネントが型安全に書ける。
  • tsとの親和性抜群。

というメリットがあるため、サーバーサイドでのちょっとしたテンプレート利用でも活用していいのでは?と思いました。