Open16

CSS-in-JS ライブラリ Linaria を触って理解する

ピン留めされたアイテム
izuminizumin

なぜ styled-components 風の CSS-in-JS が良いと思っているか

  • object style と比較して、そのままの CSS が書ける
    • camelize したりしなくていいので、CSS のエコシステムに乗りやすい
      • e.g. stylelint
    • デザイナに CSS 書いてって言いやすい(幻想かもしれない)
    • styled-components の extension がないエディタではキツい?
  • CSS Modules と比較して、CSS の定義を利用箇所に colocation できる
    • CSS に擬似的に Scope を持ち込める
    • CSS を export しないことでより Scope が狭く・明確になる
  • その他
    • css や interpolation によるスタイルの合成

一方で、レシピサービスのフロントエンドに CSS in JS を採用した話 - クックパッド開発者ブログ で言われてるような、コンポーネントが増える・HTML Element が隠れることによるデメリットも一定あるはず。
それに対しては css prop が有効な気がしている。

izuminizumin

とりあえず Next.js with TypeScript に放り込んでみる
Next.js 本体リポジトリに Example app with linaria という便利なものがあるので、とりあえずこれをなぞる

$ yarn add linaria next-linaria
$ yarn add --dev @babel/core 
next.js.config
const withLinaria = require('next-linaria')

module.exports = withLinaria({})
izuminizumin

無事にぶっこわれた

./src/pages/_app.tsx:2:12
Syntax error: Unexpected token

  1 | import "../../styles/globals.css";
> 2 | import type { AppProps } from "next/app";
    |             ^
  3 |
  4 | function MyApp({ Component, pageProps }: AppProps) {
  5 |   return <Component {...pageProps} />;
izuminizumin

.babelrc が必要なのを見逃していた
(Example にはちゃんと存在していた…)

.babelrc
{
  "presets": ["next/babel", "linaria/babel"]
}

.babelrc で linaria の preset 追加しないといけないのはわかる
ただ、忘れると import type { ... } from "..."; でぶっ壊れるのは不思議

izuminizumin

こう↓いうのを書くと

const mdH1Css = css`
  &::before {
    content: "#";
    padding-right: 8px;
    font-size: 0.8em;
    color: rgba(0, 0, 0, 0.6);
  }

  color: rgba(0, 0, 0, 0.84);
  font-size: 36px;
`;

const MdH1: React.FC = ({ children }) => <h1 className={mdH1Css}>{children}</h1>;

yarn dev だとこう↓なる

<style>.maa7pl{color:rgba(0,0,0,0.84);font-size:36px;}.maa7pl::before{content:"#";padding-right:8px;font-size:0.8em;color:rgba(0,0,0,0.6);}</style>

<h1 class="maa7pl">Test post</h1>

yarn build && yarn start だとこう↓なる

<link rel="stylesheet" href="/_next/static/css/455a0496615195b4771b.css" data-n-p=""/>

<h1 class="maa7pl">Test post</h1>
izuminizumin

styled-components だと csscss に interpolation できるが、linaria はムリなのかも?

const textCss = css`
  color: rgba(0, 0, 0, 0.84);
`;

const mdH1Css = css`
  ${textCss}
  &::before {
    content: "#";
    padding-right: 8px;
    font-size: 0.8em;
    color: rgba(0, 0, 0, 0.6);
  }

  font-size: 36px;
`;

Error: Cannot find module '@babel/preset-env' が出る
@babel/preset-envnext/babel が持っていたはず…

izuminizumin

interpolation 先が css だろうと styled.h1 だろうと変わらんっぽい

const textCss = css`
  color: rgba(0, 0, 0, 0.84);
`;

const MdH1 = styled.h1`
  ${textCss}
  &::before {
    content: "#";
    padding-right: 8px;
    font-size: 0.8em;
    color: rgba(0, 0, 0, 0.6);
  }

  font-size: 36px;
`;
izuminizumin

型を見たら string だったので、まあそうだねという感じ
css がどのようにバベられてるのかは後で確認したい

izuminizumin

css で dynamic な style は作れないらしい
styled-components でいうこう↓いうやつ

type TextStyle = "body1" | "body2";
const textCss = css<{ textStyle: TextStyle }>`
  font-size: ${props => fontSizeMap[props.textStyle]}px;
`;

ドキュメントによると、CSS Custom Properties を使うといいとのこと
https://github.com/callstack/linaria/blob/v2.1.0/docs/DYNAMIC_STYLES.md

const textCss = css`
  font-size: var(--font-size);
`;

export function Post({ title, body }) {
  return (
    <article>
      <h1 style={{ "--font-size": fontSizeMap["h1"] }} className={textCss}>{title}</h1>
      <div style={{ "--font-size": fontSizeMap["body2"] }} className={textCss}>{body}</div>
    </article>
  );
}

CSS Custom Properties については IE 除けば一番遅い Edge でも2017年10月に対応してる。
https://caniuse.com/css-variables

izuminizumin

CSS Custom Properties 使うにしてもパラメタが型に現れないのは厳しいので、まるまる Hook に隠蔽するのがまだ現実的か

const textCss = css`
  font-size: var(--font-size);
`;
export const useTextStyleCss = (textStyle: TextStyle) => {
  return {
    className: textCss,
    style: { "--font-size": `${fontSizeMap[textStyle]}px` },
  };
}
izuminizumin

CSS Custom Properties については、そもそも styled での dynamic style(props わたすやつ)でも利用されているらしい。

When using the styled tag, dynamic interpolations will be replaced with CSS custom properties. References to constants in the scope will also be inlined. If the same expression is used multiple times, the plugin will create a single CSS custom property for those.

linaria/HOW_IT_WORKS.md at v2.1.0 · callstack/linaria

小規模なアプリでの利用を除き、Linaria 全体が Custom Properties 前提と考えて良さそう

izuminizumin

css が返すのが classname なのが悩ましい
この API を採った時点で別の css を mixin した css みたいなのが実現しづらくなってる気がする
あと、利用側での classNae の記述順によって挙動が変わりうるのもお悩みポイント