LoginSignup
1
3

More than 5 years have passed since last update.

ずぼらによるずぼらのための css-in-js 入門 2019

Last updated at Posted at 2018-11-18

この記事について

ずぼらによるずぼらのための css-in-js 入門 - Qiita という記事で、css-in-js について書いたことがありますが、HTML5.2 では body 要素内に style 要素を追加してもよくなりましたので(HTML5.2で追加される機能をご紹介 - DMM inside など参照)、あそこまで凝ったことをやる必要はなくなりました。
すなわち、「JavaScript の中に css を書いちまおう!」という純粋な意味での css-in-js は、React 本体さえあれば実現できてしまいます。

サンプルソース 0.0:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>React Scroll Shadow</title>
  </head>
  <body>
    <div id="app"></div>
  </body>

  <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

  <script>

// 背景色
const backgroundColor = '#fff';

// レフトマージン
const leftMargin = '20px';

// 影を出すブラウザの幅のしきい値
const responseWidth = 400;

const css = `
  body {
    background-color: #000;
  }
  .wrapper {
    padding: 30px 30px;
    background-color: ${backgroundColor};
  }
  .box {
    /* コメントも書けます */
  }
  .shadow {
    overflow-x: auto;
    overflow-y: hidden;
  }
  @media screen and (max-width : ${responseWidth}px) {
    .shadow {
      mask-image: linear-gradient(to right, transparent,
        ${backgroundColor} ${leftMargin}, ${backgroundColor} 90%, transparent);
    }
  }
  .long {
    display: flex;    
  }
  .item {
    padding-top: 10px;
    padding-bottom: 10px;
    padding-left: ${leftMargin};
    padding-right: 20px;
    white-space: nowrap;
  }
`

const re = React.createElement(React.Fragment, {}, [
  React.createElement('style', {}, css),
  React.createElement('div', {className: 'wrapper'},
    React.createElement('div', {className: 'back'},
      React.createElement('div', {className: 'box'},
        React.createElement('div', {className: 'shadow'},
          React.createElement('div', {className: 'long'}, [
            React.createElement('div', {className: 'item'}, 'あいうえお'),
            React.createElement('div', {className: 'item'}, 'ABCDEFG'),
            React.createElement('div', {className: 'item'}, 'いろはにほへと'),
          ])
        )
      )
    )
  )
]);
ReactDOM.render(re, document.getElementById('app')); 

  </script>
</html>

ソースは短いですが、 reactjs.org でも使われている、「スマホユーザーに対しスクロール可能部分を影で知らせる表現」の実用的なサンプルとなっています。
ブラウザの横幅をスマホほどまで縮めると影が現れます。

ちなみに、const re = ・・・ 以下の部分を JSX 記法にすると、

const re = (
  <>
    <style>
      {css}
    </style>
    <div className="wrapper">
      <div className="back">
        <div className="box">
          <div className="shadow">
            <div className="long">
              <div className="item">あいうえお</div>
              <div className="item">ABCDEFG</div>
              <div className="item">いろはにほへと</div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </>
);

・・・となります。まあ、いちいちこんなこと書かなくても、ずぼらシリーズを読んだことのある方ならすぐに分かりますよね :D。

IEなどの、HTML5.2 勧告前の古いブラウザでの対応が心配になりますが、僕が試す限り特に問題なさそうです(ちなみに上記サンプルは テンプレート文字列 を使用していますので IE では動きません)。
また、「body 要素内に style タグを置くと実行速度がね・・・」みたいな話もありますが、React のような仮想 DOM システムでは、仕組み的に気にするほどのものではない気がします。

いずれにせよ、一般的なアプリでしたら、上記のように style タグを使った書き方さえ知っておけば十分でしょう。
もっとも、アプリの規模が大きくなってくると、css クラス名の衝突という点が心配になってきたりします。
そうすると、「そろそろ本格的な css-in-js ライブらい使ったほうが良いかな・・・」と思ったりするわけです。あるいは、大規模アプリを受注するであろうその日を夢見て準備をしておこう、と思ったりもするかもしれません。

そこで、人気のある styled-components あたりを試用したりして見るわけですが、なんか今度は大げさすぎるような気がして今ひとつ踏み出せない方、いるのではないでしょうか。
「css クラス名の衝突の回避さえできればいいだけなのに、方向としてはクラス名自体を使わないようにして、タグ(コンポーネント)やプロパティに置き換えていく方向なのか・・・う~ん、確かに理想ではあるけどね・・・」とか、「ナイフで済むところにチェンソーを使うのか・・・」みたいな感じです。
同じような気持ちになった方はこの記事をさらに読み進めれば幸せになれるかもしれません。
逆に styled-components をバリバリ使いこなしているような方がこの記事を読んでもおそらく時間の無駄であると思います(^^;)

ヒントは styled-components のドキュメントにあり

そうはいっても、別に styled-components が悪いわけではないし、必要な機能だけ使ってあとは無視しとけばいいだけであると思いつつ、styled-components のドキュメントを、ゴロンゴロ~ンしながら読んでいると次のような記述がありました。

The preprocessor we use, stylis, supports scss-like syntax for automatically nesting styles.

styled-components は、どうも内部で、stylis というライブラリを使っているみたいです。
「なんだそれなら、stylis を直接使えば自分好みのものができそうじゃん」僕の頭の中でにわかにアラームが響き始めました。

まずは作ってみた 1.0

さきほどのサンプルソース 0.0 を stylis を使って書き換えてみました。
ソースの終わりの方はさすがに JSX 記法の方が見やすいので、拙作 rx7.js を使用してみました。僕の拙ライブラリを使えと強要しているわけではありませんので悪しからず。

サンプルソース 1.0:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>Stylist</title>
  </head>
  <body>
    <div id="app"></div>
  </body>

  <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/rx7@latest/rx7.js"></script>
  <script crossorigin src="https://unpkg.com/stylis@latest/stylis.min.js"></script>

  <script>

function stylist(src, mark) {
  const symbol = Symbol();
  const id = `-stylist-${++stylist.counter}-`;
  const css = stylis((mark||'.')+id, src);
  stylist[symbol] = {id, css};
  return symbol;
}

stylist.counter = 0;

// 背景色
const backgroundColor = '#fff';

// レフトマージン
const leftMargin = '20px';

// 影を出すブラウザの幅のしきい値
const responseWidth = 400;

const symCss = stylist(`
  :global(body) {
    background-color: #000;
  }
  & {
    padding: 30px 30px;
    background-color: ${backgroundColor};
  }
  .box {
    /* コメントも書けます */
  }
  .shadow {
    overflow-x: auto;
    overflow-y: hidden;
  }
  @media screen and (max-width : ${responseWidth}px) {
    .shadow {
      mask-image: linear-gradient(to right, transparent,
        ${backgroundColor} ${leftMargin}, ${backgroundColor} 90%, transparent);
    }
  }
  .long {
    display: flex;    
  }
  .item {
    padding-top: 10px;
    padding-bottom: 10px;
    padding-left: ${leftMargin};
    padding-right: 20px;
    white-space: nowrap;
  }
`);

const re = rx7`
  <>
    <style>
      ${stylist[symCss].css}
    </style>
    <div class=${stylist[symCss].id}>
      <div class="box">
        <div class="shadow">
          <div class="long">
            <div class="item">あいうえお</div>
            <div class="item">ABCDEFG</div>
            <div class="item">いろはにほへと</div>
          </div>
        </div>
      </div>
    </div>
  </>
`;
ReactDOM.render(re, document.getElementById('app')); 

  </script>
</html>

改良してみた 2.0

stylis を使っただけであっさりと css クラス名のローカル化ができてしまいました。
これでも十分なのですが、React らしくコンポーネントも作ってみたくなりました。改良してみましょう。

サンプルソース 2.0:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width">
    <title>Stylist</title>
  </head>
  <body>
    <div id="app"></div>
  </body>

  <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
  <script crossorigin src="https://unpkg.com/rx7@latest/rx7.js"></script>
  <script crossorigin src="https://unpkg.com/stylis@latest/stylis.min.js"></script>

  <script>

function stylist(src, mark) {
  const symbol = Symbol();
  const id = `-stylist-${++stylist.counter}-`;
  const css = stylis((mark||'.')+id, src);
  stylist[symbol] = {id, css};
  return symbol;
}

stylist.counter = 0;

class Stylist extends React.Component {
  constructor(props) {
    super(props);
    this.symbol = stylist(props.css);
    this.style = stylist[this.symbol];
  }
  componentWillUnmount() {
    delete stylist[this.symbol];
  } 
  render() {
    const props = this.props;
    return React.createElement(
      props.tag || 'div',
      {className: this.style.id},
      [
        React.createElement('style', {}, this.style.css),
        props.children
      ]
    );
  }
}

// 背景色
const backgroundColor = '#fff';

// レフトマージン
const leftMargin = '20px';

// 影を出すブラウザの幅のしきい値
const responseWidth = 400;

const css = `
  :global(body) {
    background-color: #000;
  }
  & {
    padding: 30px 30px;
    background-color: ${backgroundColor};
  }
  .box {
    /* コメントも書けます */
  }
  .shadow {
    overflow-x: auto;
    overflow-y: hidden;
  }
  @media screen and (max-width : ${responseWidth}px) {
    .shadow {
      mask-image: linear-gradient(to right, transparent,
        ${backgroundColor} ${leftMargin}, ${backgroundColor} 90%, transparent);
    }
  }
  .long {
    display: flex;    
  }
  .item {
    padding-top: 10px;
    padding-bottom: 10px;
    padding-left: ${leftMargin};
    padding-right: 20px;
    white-space: nowrap;
  }
`;

const re = rx7`
  <${Stylist} css=${css}>
    <div class="box">
      <div class="shadow">
        <div class="long">
          <div class="item">あいうえお</div>
          <div class="item">ABCDEFG</div>
          <div class="item">いろはにほへと</div>
        </div>
      </div>
    </div>
  </Stylist>
`;
ReactDOM.render(re, document.getElementById('app')); 

  </script>
</html>

だいぶ React らしくなりましたね。

2.0 が 1.0 からどのように変わったのか、ぜひ追ってみてくださいね。
render 方式については、今回は styled-components と同じく children props 方式を採用しましたが、HOC 方式、children function 方式、あるいは、流行?の render props 方式などお気に入りの render 方式でいろいろ実装してみてください。
ちなみに僕は HOC 方式はソースの可読性を低めるので大嫌いです(笑)。

  • Hard for humans
  • Hard for machines

By Sophie Alpert

2.0 でコンポーネント化してはみたものの、個人的には 1.0 のまま使うかな。
1.0 のままの方が何かと応用が効きそうですし。
React を使うからといって何でもかんでもコンポーネント化すればいいというものではありません。「React でのコーディングはこうあるべし」みたいな考え方には僕は断固反対します。
僕は、React が多種多様性を尊重しようとしているからこそ好きなのです。

最後に

いかがでしたでしょうか。
誤解を恐れずにいってしまえば、css-in-js とは所詮この程度のものです。
ライブラリの選定の参考にしてください。

ソースファイルのライセンスは MIT です。ご自由にお使いください。

少しでも皆様の力になれればうれしいです。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3