styled-components や Emotion のあれはタグ付きテンプレート

styled-components や Emotion のあれはタグ付きテンプレート

December 11, 2021

CSS in JS の代表的なライブラリである styled-components や Emotion では以下のような書き方でスタイルを定義します。

const Button = styled.button`
  padding: 8px;
  background-color: #ff6699;
  color: #ffffff;
`;
const button = css`
  padding: 8px;
  background-color: #ff6699;
  color: #ffffff;
`;

上記に登場する以下ですが、

styled.button``;

css``;

お恥ずかしながらこれがどのように機能しているのか理解せずに使っていたので、今回整理します。

タグ付きテンプレート #

「``」はタグ付きテンプレートです。

タグ付きテンプレート

タグ付きテンプレートは、テンプレートリテラルのより高度な形式です。

タグを使用すると、テンプレートリテラルを関数で解析できます。タグ関数の最初の引数には、文字列リテラルの配列を含みます。残りの引数は式に関連付けられます。

テンプレートリテラル (テンプレート文字列) - JavaScript | MDN

テンプレートリテラルを知らない人は少ないと思いますが、タグ付きテンプレートなんてものも存在したのですね。

通常の関数呼び出し方とタグ付きテンプレートで比べてみる #

通常の呼び出しとタグ付きテンプレート形式の呼び出し方で関数の結果を見比べます。

const print = (args) => console.log(args);

print('hello'); // hello
print`hello`; // [ 'hello' ]

タグ付きテンプレートの方では、配列として引数が渡されたようです。

では次に以下を試してみましょう。

const print = (args) => console.log(args);

const message = 'world';

print(`hello ${message}`); // hello world
print`hello ${message}`; // [ 'hello ', '' ]

タグ付きテンプレートでは world が出力されていません。

world がどこに行ったのかは print 関数を次の通り修正するとわかります。

const print = (...args) => console.log(args);

const message = 'world';

print(`hello ${message}`); // [ 'hello world' ]
print`hello ${message}`; // [ [ 'hello ', '' ], 'world' ]

world は第二引数として渡されていることがわかりました。

テンプレートリテラルの部分を増やしてみます。

const print = (...args) => console.log(args);

const message1 = 'world';
const message2 = 'again';

print(`hello ${message1} ${message2}`); // [ 'hello world again' ]
print`hello ${message1} ${message2}`; // [ [ 'hello ', ' ', '' ], 'world', 'again' ]

上記から、以下のルールで引数が渡されていることがわかります。

print`hello ${message1} ${message2}`;

// 以下が第一引数として配列で渡される
// - 先頭から最初のテンプレートリテラル(${message1})開始までの 'hello '
// - 上記終わりから次のテンプレートリテラル(${message2})開始までの ' '
// - 上記の終わりから最後までの ''
// -> 第一引数 ['hello ', ' ', '']

// テンプレートリテラルで記述された内容は第二引数以降でそれぞれ渡される
// -> 第二引数 'world'
// -> 第三引数 'again'

さて、ここまでだといまいち使い所のわからないタグ付きテンプレートですが、テンプレートリテラル内に関数を記述した場合に大きな違いがあります。

const print = (...args) => console.log(args);

print(`hello ${() => console.log('world')}`); // [ "hello () => console.log('world')" ]
print`hello ${() => console.log('world')}`; // [ [ 'hello ', '' ], () => console.log('world') ]

通常の呼び出し方では "() => console.log('world')" という文字列として渡されているのに対し、タグ付きテンプレートでは () => console.log('world') という関数そのものとして渡すことができています。

タグ付きテンプレートだと関数を関数として渡すことができる #

つまり以下のように関数を実行することができます。

const exec = (...args) => {
  args.forEach((arg) => {
    if (typeof arg === 'function') {
      arg();
    }
  });
};

exec`hello ${() => console.log('world')}`; // console.log('world') が実行され world と出力される

では今度は以下のようにしてみましょう。

const styling = (arg1, arg2) => {
  return `${arg1[0].trim()}${arg2()};`;
};

let bool;

bool = true;

const style1 = styling`
  color: ${() => (bool ? 'red' : 'blue')}
`;

bool = false;

const style2 = styling`
  color: ${() => (bool ? 'red' : 'blue')}
`;

console.log(style1); // color: red;
console.log(style2); // color: blue;

なんだかいつも使っている CSS in JS の記述に似ていませんか?

以上 #

ライブラリを作らない限り、自分でタグ付きテンプレートを利用したコードを書く機会は多くないかもしれませんが、せっかくなのでぜひ覚えておきましょう。