親から子へ値を渡す props

JSX で HTML のように属性値を渡すとkeyref(React が扱う値)以外はpropsとして子コンポーネントに渡せます。

です。

const Child = props => {
  return <p>{props.text || "default text"}</p>;
};

const App = () => {
  return (
    <div className="App">
      <Child text="props text" />
      <Child />
    </div>
  );
};

Childtext属性を使っているので、親が使うときにtext属性を置いてあげるとその値を使うことができます。ですが、この値はが必ず渡されるという保証はありません。
予期しない動作にならないように、子ではフォールバック値が使われるように|| "default text"のように設定しておくと安心です。

JSX のタグ名にはどんなものが使えるかまとめ

公式ドキュメントには、「頭に小文字を使うと HTML かと思うので、大文字で定義してください」というようなことがこの辺に書かれていますが、少し分からないケースがでてきたので自分でもトランスパイルしてみて調べました。

変数

この辺はドキュメントの通り頭小文字で定義した場合は単に文字列として渡されていました。

const orange = () => <div style={{padding:10,background:'orange'}} />;
const Orange = () => <div style={{padding:10,background:'orange'}} />;

展開後。

React.createElement('orange', {
  style: {padding: 10, background: 'orange'}
});
React.createElement(Orange, {
  style: {padding: 10, background: 'orange'}
});

オブジェクト

プロパティアクセス

ここが良く分からなかった部分です。
オブジェクトの場合は、大文字小文字関係なく React コンポーネントとして渡されます。

const div = {}
div.orange = () => <div style={{padding:10,background:'orange'}} />;
div.Orange = () => <div style={{padding:10,background:'orange'}} />;

展開後。

React.createElement(div.oarnge, {
  style: {padding: 10, background: 'orange'}
});
React.createElement(div.Orange, {
  style: {padding: 10, background: 'orange'}
});

ちなみにこの場合にdivを使っても React はそれを単に HTML タグと考えらるので問題ないです。

// 上の続き
<div />

// は
React.createElement('div', null);

インデックス記法でのアクセス

これもまぁドキュメントに書かれているのですが、div['orange']というような形を JSX のタグで使うことはできません。その場合は事前にOrangeのような変数にその値を格納してから<Orange />のように使う必要があります。

React Hooks ∋ useState

コンポーネント内部の状態を持つ為に使います。この関数は初期値を引数として渡せ、現在値と更新用の関数を返します。

以下は例です。

const [hidden, setHidden] = useState(false);

hidden値とsetHidden関数を得ます。単にこれは分割代入なので名前は適当に決めます。

定義した直後hiddenは引数として渡したfalseが入ります。この値をtrueにするにはsetHidden関数に次の値を渡して実行します。

setHidden(true);

更新用の関数は、引数に関数を取ることもできます。引数の関数には現在値が渡されて実行され、戻り値が次の現在値になります。なので、以下のようにも書けます。

setHidden(state => !state)

値を更新すると、コンポーネントの状態を更新したという事になるので、このフックを持つコンポーネントは再描画されます。

React Hooks ∋ useReducer

コンポーネントを Flux フロで管理したいならuseStateの代わりに使うのがいいかもしれません。特に管理する状態が多い場合はこちらを使うと処理を外部に切り分けられコードが見やすくなるのでお勧めします。

このフックは2-3の引数を受け取ります。1つ目は Reducer、2つ目はその Reducer の初期値、3つ目はオプショナルで2つ目の初期値が複雑な場合、それを更に弄る為の関数です。このフックは戻り位置に状態statedispatchという Reducer へアクションを流すための関数を返します。

です。

const init = initialState => {
  initialState.count = Math.floor(Math.random() * 10);

  return initialState;
};

const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";

const initialCountState = { count: 0 };

const countReducer = (state, action) => {
  switch (action.type) {
    case INCREMENT: {
      const nextState = { ...state };
      nextState.count++;
      return nextState;
    }

    case DECREMENT: {
      const nextState = { ...state };
      nextState.count--;
      return nextState;
    }

    default: {
      return state;
    }
  }
};

const App = () => {
  const [state, dispatch] = useReducer(countReducer, initialCountState, init);

  const increment = useCallback(() => {
    dispatch({
      type: INCREMENT
    });
  }, [dispatch]);

  const decrement = useCallback(() => {
    dispatch({
      type: DECREMENT
    });
  }, [dispatch]);

  return (
    <div className="App">
      <span>{state.count}</span>
      <button onClick={increment}>increment</button>
      <button onClick={decrement}>decrement</button>
    </div>
  );
};

useReducerには上記の説明のように(ここでは無理やり)3つの引数を渡してます。initialStateではcount0としてますが、init関数により0から9のどれかに書き換えられます。

アクションを発行する関数はuseCallbackで囲みます。この Reducer を使った更新ではdispatch関数は毎回変更が入ってしまい毎回アクション関数を新しく作ることになってしまいますが、依存しないpropsの更新が来た時などにはそれを防げる為です。今回はpropsがありませんが、そういった場合でも癖として常にやっておいたほうがいいかなと思います。

<button>をクリックするとcountReducerへアクションが飛びます。countReducerではそのアクションのtypeを見て捌きます。switch構文で捌くことが多いですが、if構文などでも構いません。

そしてcountReducerの戻り値が次のuseReducerの戻り値のstateへやってきます。そして、その値でコンポーネントが再描画される…という動作を繰り返します。

React Hooks ∋ useCallback

条件が同じ時に同じ関数を返すことができる関数です。1つ目の引数にある条件の時の関数、2つ目の引数に条件を渡します。戻り値もまた関数でその引数や戻り値は1つ目の引数に渡した関数と同じになります。

以下は例です。

const fn = useCallback(
  () => {
    console.log('called with: ', foo, bar, baz);
  },
  [foo, bar, baz]
);

まず前提として JavaScript では(() => {}) !== (() => {})です。これは参照先が違う為です。

上記コードのfnfoobarbazに依存してます。例えば

  1. foo === 1
  2. bar === 2
  3. baz === 3

の時、関数は'called with: 1 2 3'表示する関数を返します。さて、このような動作の関数は何個も必要なのか。やってることは同じなので1つ'called with: 1 2 3'表示する関数を作れば十分なはずです。それを1つにしちゃうのがuseCallbackです。

つまり、useCallbackがやっているのはfoobarbazがセットで同じ場合、前回作った関数があればそれを使うという事をするフックという事になります。

依存無しの関数

もし関数の内部が外部のどんな変数にも依存してない場合、明らかに生成が必要な関数は1つになります。明らかに1つの時は条件に空配列を置きます。

useCallback(
  () => { /* ... */ },
  []
);

これで常に最初に渡した() => { /* ... */ }が返ります。

依存無しなのにわざわざuseCallbackを使う必要があるのかとも思うかもしれません。これは必要あります。でなければ、子コンポーネントに渡すときに違う参照の関数を渡すことになり、子コンポーネントから見れば毎回違うセットのpropsが渡ってくるように見える為です。

React Hooks ∋ useMemo

ほぼuseCallbackと同じ役割です。主に参照を返すような値をいくつも作らないようにする為に使います。

useMemoには1つ目の引数に好きな値、2つ目にはuseCallbackなどと同じ用に条件を渡します。この条件がセットで同じ値の時は前回返した値を返します。

以下は例です。

const obj = useMemo(
  {foo, bar, baz},
  [foo, bar, baz]
);

例えば、

  1. foo === 1
  2. bar === 2
  3. baz === 3

な時obj{foo: 1, bar: 2, baz: 3}というオブジェクトを返します。次に同じ条件のセットで再度実行されるとobj前回返した{foo: 1, bar: 2, baz: 3}をそのまま返します。もし違う条件のセットであれば新しいオブジェクトを作って返します。

依存無しの値

もし値が外部のどんな変数にも依存してない場合、明らかに生成が必要な値は1つになります。明らかに1つの時は条件に空配列を置きます。

useMemo(
  { /* ... */ },
  []
);

これで常に{ /* ... */ } === { /* ... */ }な値が返ります。

依存無しなのにわざわざuseMemoを使う必要があるのかとも思うかもしれません。これは必要あります。でなければ、子コンポーネントに渡すときに違う参照の値を渡すことになり、子コンポーネントから見れば毎回違うセットのpropsが渡ってくるように見える為です。

React Hooks ∋ useEffect

コンポーネントのライフサイクル時に処理を行うために使います。ライフサイクルとはコンポーネントが、

  1. レンダリングされた時
  2. リレンダリング(更新)された時
  3. 削除される時

です。

useEffectは引数に2つの値を渡します。1つ目は上記のライフサイクル処理を行う関数、2つ目はライフサイクル時に関数を発火するかどうかの依存条件をuseCallbackらと同じように渡します。戻り値はありません。

以下は例です。

const [foo] = useState('');

useEffect(
  () => {
    console.log(foo);
  }, [foo]
);

useEffectはレンダリング後に1回は必ず呼ばれます。その後は引数の条件によって発火するかが変わります。このコードではfooが依存条件としてセットされているのでfoo更新された後にも発火されます。

ライフサイクルの1と2は出てきましたが3はどう使うのか。実は、上記のコードではこれは実装されていません。3を行うには1つ目の引数の関数を修正する必要があります。それは、関数の戻り値として関数を返す事です。

少し書き換えます。

const [foo] = useState('');

useEffect(
  () => {
    console.log(foo);
    
    return () => {
      console.log('unmount');
    }
  }, [foo]
);

これでこのコンポーネントが削除される際にunmountとコンソールに表示されるはずです。

2以外を実装

つまり更新時の処理はいらないを実現する方法です。これは依存条件に空配列を渡すだけで実装できます。
「最初の1回は必ず実行され…最後にも1回…」

useEffect(
  () => () => { /* ... */ }
  []
);

ここから3の削除時のライフサイクルもいらない場合はどうするか。関数を返さなければいいですね。

useEffect(
  () => { /* ... */ }
  []
);

useLayoutEffect

ちなみにこちらの似たフックは、レンダリングされて反映される直前に実行されるライフサイクルです。ライフサイクルの順番が早いのはuseLayoutEffect > useEffectです。

React Hooks ∋ useRef

コンポーネントで返した HTML 要素を使う時に使います。HTML 要素へ渡す場合、基本的に初期値はnullを渡します。

const buttonRef = useRef(null);

要素はbuttonRef.currentに入りますが、コンポーネントがレンダリングされるまではその値はnullです。レンダリング後は狙った要素の参照が入ります。

準備

buttonRef.currentにほしい要素のref属性にこのbuttonRefを渡してあげます。

<button ref={buttonRef>
  foo
</button>

React Hooks ∋ useImperativeHandle

useRefと同じ様な使用用途です。useRefはコンポーネント内の HTML 要素を触る為のフックでしたが、useImperativeHandle親コンポーネントにコンポーネント内の要素を触る為の手段を提供する為のフックです。React.forwardRefとの違いは2つほどあると思います。

  1. コンポーネント内の要素を親コンポーネントから直接触られない
  2. コンポーネント内の複数の要素(参照)を親コンポーネントは触ることができる

例。このフックは親から参照をもらうためにforwardRefと共に使います。

const Checkbox = forwardRef((_, ref) => {
  const checkboxRef = useRef(null);

  useImperativeHandle(ref, () => ({
    check() {
      if (checkboxRef.current !== null) {
        checkboxRef.current.checked = true;
      }
    }
  }));

  return <input ref={checkboxRef} type="checkbox" />;
});

こうして渡ってきた親コンポーネントからのref.currentに対して、useImperativeHandleハンドラーを生やします。この例ではcheckメソッドを持つオブジェクトを返していますが、これがまさにref.current.checkを実装していることになります。
この中のcheckboxRefは要素の為のuseRefなので普段通りに使えます。

親コンポーネントからref.current.check()が呼ばれるとコンポーネント内のinput[type=checkbox]のチェックフラグをtrueに書き換えます。(React にはもっと優れた方法があるので実際にはこのように実装しないように)

親コンポーネント

親です。親では何度も言っているref.current.check()の実行の仕組みとハンドラーを生やすためのrefforwardRefなコンポーネントに渡す必要があります。

export default () => {
  const ref = React.useRef(null);
  const onClick = React.useCallback(() => {
    ref.current.check();
  }, []);

  return (
    <div className="App">
      <Checkbox ref={ref} />
      <button onClick={onClick}>check!</button>
    </div>
  );
};

forwardRefに渡すuseRefも基本的にnullでフォールバック値を与えます。これは子要素が存在しない場合はref.currentにはnullが入るようにする為です。

check!ボタンを押してinput[type=checkbox]にチェックが入るか確認できたら完了です。

ユースケース

Pulldown 実装

タブ移動をサポートしないといけないようなアプリケーションの時focus要素の操作にuseImperativeHandleは便利かもしれません。これは実装です。

この要素は、Pulldown にフォーカスが入った後サジェストが開き、その後サジェストの最後の要素が抜けるまでその状態を維持し、抜けたら Pulldown を閉じるというような動作が期待されます。

サジェストの要素どれかにフォーカスがあたってるかをuseImperativeHandleを使って親から調べれるようにすると、親要素である Pulldown の開く・閉じるタイミングが簡単に制御できるようになります。また子の参照だけではなく上の方で書いた Checkbox のように DOM を操作することもできるので、例のように子がそういうハンドラーを提供すれば、親から Focus している要素を変更するといった処理も行えます。

子要素参照 React.forwardRef

子コンポーネントの適当なものと紐付いた参照を親コンポーネントから覗きたい場合に使います。React.forwardRefでラップするコンポーネントはpropsの他にrefを2つ目の引数に受け取ります。この中身は親コンポーネントのどこかで生成された参照です。

以下は1秒後にチェックボックスをチェックするです。

const Child = React.forwardRef((_, ref) => {
  return <input ref={ref} type="checkbox" />;
});

const App = () => {
  const ref = React.useRef(null);

  React.useEffect(() => {
    setTimeout(() => {
      ref.current.checked = true;
    }, 1000);
  }, []);

  return <Child ref={ref} />;
}

Childに対してReact.forwardRefを使ってます。そして渡されたrefinput[type=checkbox]に渡すことでこの要素を親から直接触れるようにしてます。

Appでは描画が完了後にそのrefを通してinput[type=checkbox]checkedを真にすることでチェック済の状態に DOM を更新してます。

Context

React のコンテキストは、propsを使わずに子コンポーネントに親のデータを渡す為に使います。これにより

  • 複数のコンポーネントから成り立つコンポーネント間のpropsが簡単になる
  • 子が特定の親の下に置かれてるどうか確認できる

などの利点があります。

コンテキストを作るにはReact.createContextを使います。これはデフォルトで渡されることになる値を引数として1つ受け取ります。

const Meta = React.createContext({title: 'blog title'});

このMetaはオブジェクトでProviderConsumer2つのコンポーネントを持っています。
まずProviderは渡すデータを置くために使います。データはこの要素のvalue属性に渡します。 次にConsumerは渡ってきた値を受け取る為の要素です。
Consumerは上位階層にProviderがある時、一番近いそれに渡された値を受け取りますが、もしProviderが1つも無い場合、コンテキストを作る時に渡したデフォルト値を受け取ります。注意なのがこのコンポーネントのchildrenはデータを引数に受け取る関数だという点です。

です。この例はProviderと通す時と通さない時の動作を見れるもので、それぞれpropsで何も値を渡していないのにapp titleblog titleと別の内容が表示されます。

const Meta = React.createContext({ title: "blog title" });

const Title = () => {
  return (
    <Meta.Consumer>
      {meta => {
        return <h1>{meta.title}</h1>;
      }}
    </Meta.Consumer>
  );
};

const App = () => {
  return (
    <>
      <Title />
      <Meta.Provider value={{ title: "app title" }}>
        <Title />
      </Meta.Provider>
    </>
  );
}

useContext

Consumerの代わりに、useContextフックを使う方法もあります。このフックを使って上記のコードを書き換えるとこうなります。

const Meta = React.createContext({ title: "blog title" });

const Title = () => {
  const meta = React.useContext(Meta);

  return <h1>{meta.title}</h1>;
};

export default function App() {
  return (
    <>
      <Title />
      <Meta.Provider value={{ title: "app title" }}>
        <Title />
      </Meta.Provider>
    </>
  );
}

useContextへはConsumerを渡すわけではなくコンテキストそのものを渡します。

考察

memo化されたコンポーネントの中でuseContextを使っていると、Contextの値を更新してもそのコンポーネントは再レンダリングされません。このことからContextの全ての値を更新することはオススメしないです。

僕的にはContextへは何かキーの下にぶら下がれるオブジェクト値などを格納すると良いと思います。例えば以下のような感じです。

const contextValues = {
  '/': {
    title: 'Top Page',
    description: '...'
  },
  '/foo': {
    title: 'Foo Page',
    description: '...'
  },
}

そしてこのキー(path)だけ Flux で管理して必要に応じてuseContextを使っているコンポーネントに渡します。コンポーネントの中では渡されたpathを使いuseContextから値を取り出して使います。

const {title, description} = useContext(XxContext)[path];

このようにすることでmemo化されていても値を更新できますし、その第2引数を渡してない場合は、デフォルトで行われる浅い比較の回数も減らせれます。

React.memo で更新制御

React.memoで関数をラップすると、関数にpropsが渡ってくる前に「更新必要そうか?」が確認されるようになります。もちろん、「更新必要なさそう」と判断された場合は前回返されたコンポーネントの結果が使われます。

例。

const Foo = React.memo(props => {
  return <div>{props.text}</div>
});

これは以下と同等です。

const Foo = React.memo(
  props => {
    return <div>{props.text}</div>
  },
  (prevProps, nextProps) => {
    return prevProps.text === nextProps.text;
  }
);

1つ目の引数に渡すのは単に Functional Component ですが、2つ目の引数には前回のpropsと今回のpropsを使って何らかの確認を行う関数を渡します。この関数がfalseを返すと「更新必要そう」と判断されます。

また2つ目の関数は省略可能で、渡さなかった場合は浅い比較での比較(オブジェクトの第一階層同士のチェック)を行う関数になります。

この部分をできるだけデフォルトの浅い比較で済ますためにuseMemouseCallbackなどのフックが役立ちます。直前に生成したオブジェクト値などをプロパティに渡すと、参照同士の比較で常にfalseになってしまいますが、メモ関数から返された値を使うことで場合によってtrueとなる値を取得することができます。つまり、メモ関数によってオブジェクトが同じかどうかはメモの結果により既に求められている為、この部分の比較を簡略化することができます。

CodeSandbox上にを作りました。ボタンをクリックすると親階層の状態が更新される事により子コンポーネントも更新されようとしますが、浅い比較によりMemoFooではコンポーネントが更新されない事が確認できます。

空の要素 React.Fragment

React.Fragment要素を使うとどうしてもネストしなければ表現できないような構造時にラップする要素として使えます。この要素は何も HTML 要素を生み出しません。

この要素は要素として特別に扱われ、<></>で囲む方法が許されてます。

<>
  <div>contents1</div>
  <div>contents2</div>
</>

このコードは実際には以下のような結果となります。

<div>contents1</div>
<div>contents2</div>

属性を渡したい

ただ<></>では属性を渡すことはできません。例えば代表的なものにkey属性があります。その場合は普通にReact.Fragmentを要素として使います。

{[...Array(5)].map((_, i) => {
  return (
    <React.Fragment key=  {i}>
      <div>contents1</div>
      <div>contents2</div>
    </React.Fragment>
  );
})}

コード分割

React.SuspenseReact.lazyの2つを一緒に使うことで、必要になった時に初めてコードを読み込んでコンポーネントとしてレンダリングするというフローを簡単に実装できます。

⚠️ この要素は SSR (サーバーサイドレンダリング)では使えません。その場合はその部分をうまく SSR 時には実行しないような処理として書く必要があります。

です。

まず適当に動的に読み込むコンポーネントを作ります。以下をlazy-child.jsとします。

import React from "react";

export default () => <div>Lazy Child</div>;

このコンポーネントを使って試してみます。

const LazyChild = React.lazy(() => import("./lazy-child"));

const App = () => {
  return (
    <React.Suspense fallback={<div>loading...</div>}>
      <LazyChild />
    </React.Suspense>
  );
}

LazyChildではReact.lazyでラップしてlazy-child.jsを動的に読み込む関数を中に置いてます。返される値はコンポーネントですが、このコンポーネントは必ずReact.Suspense以下に配置する必要があります。
もしそうしない場合A React component suspended while rendering, but no fallback UI was specified.というエラーになります。

React.Suspenseにはfallback属性を渡します。これはコードを読み込んでいる最中に変わりに表示される要素になります。この動作を分かりやすくする為に一時的に以下のように書き換えてみると分かりやすいです。これはわざと読み込みに2秒以上かかるようにしたコードです。

const LazyChild = React.lazy(async () => {
  await new Promise(r => {
    setTimeout(r, 2000);
  });
  return import("./lazy-child");
});

TypeScript と共に使う時に className にリテラル型のユニオンを設定する

jsx で何の気なしにclassName属性を使っている時の型はReact.HTMLAttributesの中にclassName?: stringと定義(@types/react@16.9.2では@types/react/index.d.ts:1628辺り)されています。

また例えば<div>タグを使った時の型はdiv: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>;と定義(JSX.IntrinsicElements@types/react/index.d.ts:2858辺り)されています。

ということで、実質React.HTMLAttributesが使われているということで、それを自分で上書きしてあげれば良さそうに感じますがdelare module 'react'で定義したとしても既存のプロパティの解決はデフォルトのもの(@types/react)が優先して使われるようなので無理です。

ではどうするかと言うと、JSX.IntrinsicElementsからすべて再定義してあげます。まずは既存のJSX.IntrinsicElementsをすべてコピーしdelare module 'react'内に置きます。

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      a: React.DetailedHTMLProps<
        React.AnchorHTMLAttributes<HTMLAnchorElement>,
        HTMLAnchorElement
      >;
    }
  }
}

あとはこのReact.AnchorHTMLAttributesを更に再定義してあげてあげれば目的は果たせそうです。ですがReact.AnchorHTMLAttributesaの場合だけで、tableだったらReact.TableHTMLAttributesinputだったらReact.InputHTMLAttributesだったりと数が多くすべてに対応していたらそれだけで一苦労です。

TypeScript には、

interface A {
   foo?: string;
}

interface B {
   foo: 'foo'
}

type C = A & B;
// C = {foo: 'foo'};

になるという法則があります。そしてすべてのReact.*HTMLAttributesReact.HTMLAttributesを拡張したものなので、React.HTMLAttributesを継承したものをジェネリクス型として受け取れる何か型を定義し、{className: 自分の型;}とすればいけそうです。

declare module 'react'の中にこのようなものも定義してみました。

type MyHTMLAttributes<T extends React.HTMLAttributes> = T & {
  className?: 'foo' | 'bar';
}

やっていることは前のコードと同じstring | undefined'foo' | 'bar'へ変換することです。あとはすべてのReact.*HTMLAttributesMyHTMLAttributesで囲めば目的が果たせそうです。

VSCode 以外はできるか分かりませんが、できるなら以下の組み合わせで一気に変換すると楽かなと思います。


\s(React.*HTMLAttributes(?!>)<[^,]*)
# から
 MyHTMLAttributes<$1>
# 先頭にスペース注意

React と AtomicDesign - Atom(原子)編

すべて個人的な考えです。都度更新。

使用される要素の最小サイズが Atom(原子)

よく例にあるのがボタン(<button>ボタン</button>)です。これは「クリックで何かしらアクションを起こす」という役割だけの要素だからです。

<pre><code>...</code></pre>はコードを表示するための構成で、2つの HTML 要素で構成されています。これは Atom なのかすが、僕は Atom に分類しています。2つの要素を使っていますが、単にコードの入れる場所って役割以外に使うことないからです。

Atom と Atom に分類し使うこともできると思いますが、それらを単体で使わないのであれば恐らく単に細分化しすぎで、やはりatom + atom = atomとなるんじゃないかと思います。「この役目を果たすための最小サイズのラインはどこだろう?」と分類する感じで考えています。

OOCSS

OOCSSというのはオブジェクト指向CSSと呼ばれるCSS設計方法で、スタイル付けをスキン(見た目)とストラクチャ(構造)に分けて考えようという設計方法です。スキンは色とか装飾系だけをまとめたセレクタのことで、ストラクチャは幅、高さ、余白とか見た目以外のスタイルをまとめたセレクタです。

Atom では「できる」を保証します。実際にどんな見た目になるかは上位層が決めます。
フォームの中では幅100%だけどヘッダーでは300pxで使いたい<input>など、こういうものは上位層でそういった構造スタイルを付けるようにします。

// molecule
const SearchOfTopMenu = () => {
  return (
    {/* ... */}
    <Input structure="fluid" skin="standard" />
    {/* ... */
  );
}

Atomic では上位層に行くほど使いまわしの回数は減るはずで、どんどん具体的な内容にもなっていくかなと思うので、具体的になってから初めて構造は付ければ良いかなと思います。

大抵デザインから Atomic を作ろうとするとあるレベルで要素をどんどん分解して最後に組み立てるだけとなってしまいがちですが、コンポーネントを作る段階ではなるべく「この要素はここで使うから」みたいな具体的な背景も頭の中から落とすよう(考えないよう)にするといいかもしれないなと思います。

ロジックを持って、状態は持たない

これはHTMLの要素の動きに近いです。

ロジックは持ってもいいです。disabledという属性値を持たせて、これが真ならアクションを起こせなくするや(まぁdisabledなら CSS で制御できそうですが)、loadingtrueならテキストをローディングアイコンにするような簡単なものなど。ただしそれらを状態として保持しないようにします。

const PostButton = ({loading = false, disabled = false, children}) => {
  return (
    <button className="..." disabled={disabled}>
      {loading ? <svg /* ローディングアイコン */ /> : children}
    </button>
  );
};
//
// 使う
// <PostButton loading disabled>投稿</PostButton>

HTML でも<input disabled placeholder="...">などで Atom 単位である程度動的に制御できるように作られています。disabledを消せばまた入力できるようになるでしょう。ロジックを持って状態は持たないのは自然だと思います。

Functional Component で定義

実装する際は、あとで拡張しやすいように最低でも Functional Component として定義するといいかと思います。あと、TypeScript を使っている場合は自分の型で完結させたほうが後々変にならないだろう(少しは)とも思います。


ここから2019-06までの内容(アーカイブ)


<div><span> のようにある要素を組み上げるための部品が Atom です。 僕はよくstyled-componentsを使いますが、これで生成したコンポーネントは全て最小単位の部品で Atom に属すと考えれます。

// FlexはAtom
const Flex = styled.div`
  display: flex;
`;
// MyFormはAtom
const MyForm = styled.form`
  /* ... */
`;
// MyInputはAtom
const MyInput = styled.input`
  /* ... */
`;

これらはこのように使えます。

<Flex>
  <MyForm>
    <MyInput />
  </MyForm>
</Flex>

Styled Components を使っていないなら単一要素の SFC と1対1で CSS ファイルを作成し管理するのがいいと思います。

foo
 ├─ index.jsx
 └─ foo.css
// atoms/index.js
const Flex = props => <div className="atom-Flex" {...props} />;
/* atoms/flex.css */
.atom-Flex {
  display: flex;
}

属性値で見た目の情報を変える

HTML の<input>readonlydisabledといった属性を渡すとスタイルが変わるようにaria-*data-*といった属性を振り、それでスタイルを振り分けます。

const Flex = styled.div`
  display: flex;

  &[aria-orientation='vertical'] {
    flex-direction: column;
  }
`;

const FlexItem = styled.div`
  flex: 0;

  & + & {
    margin-left: 1em;
  }

  &[aria-label='main'] {
    flex: 0 1 1000px;
  }
`;

// <Flex>
//   <FlexItem>...</FlexItem>
//   <FlexItem aria-label="main">
//     <Flex aria-orientation="vertical" />
//   </FlexItem>
// </Flex>

使えそうなaria-*属性一覧

追記 (2019-01) - 使えるaria-*属性一覧

と思いましたが、要素にはもともとデフォルトでroleが割り当てられていて、このroleにこのaria-*は相応しくないというようなルールがあるようです。なので適当に上の属性を当てているとその辺りで Lighthouse のアクセシビリティのスコアが結構下がってしまいます。

僕的にはaria-*を使ってのスタイル制御は分かりやすく管理しやすいと思っているので、ariadataに変えたもの(data-hidden)を今は使っています。

React と AtomicDesign - Molecule(分子)編

すべて個人的な考えです。

Atom (や Molecule)をよしなに組み合わせるための Molecule (分子)

よくフォームで例えられていたりしますね。例えばこんな構造だとします。

<SearchForm structure="top-menu" skin="standard">
  <Input structure="round" skin="standard" />
  <Button structure="round" skin="standard">検索</Button>
</SearchForm>

SearchFieldInputButtonが Atom です。OOCSS で考えてるのでstructureskinを持っています。しかし、それぞれのコンポーネントは子要素や兄弟要素との関連性をよく分かっていない要素達です。そういった要素たちのスタイルをよしなに組み合わせる為に Molecule 層で整えます。

例えばInputButtonは今の状態ではピタッとくっついている状態だとして、間にスペースを開けたいんだという場合はこんな感じにします。(ちなみに、styled-componentsです)

const Molecule = props => {
  const buttonSkin = React.useMemo(() => {
    if (props.value === '') {
      return 'disabled'
    }
    
    return 'enabled';
  }, [props.value]);

  return (
    <div style={{marginTop: 10}}>
      <SearchForm structure="fluid" skin="standard">
        <Input structure="round" skin="standard" value={props.value} />
        <div style={{marginLeft: 10}}>
          <Button structure="round" skin={buttonSkin}>
            検索
          </Button>
        </div>
      </SearchForm>
    </div>
  )
};

マージン用のdivでラップして空間を整えています。(これはstyleにしてますが、実際はコンポーネント化したものを使います)
この辺は人の好みはあると思いますが、個人的にはマージンは要素外の要因だと思っているのでそれぞれの Atom で持つのはちょっと...という考えだからです。ちゃんとしたデザインならstructureで持たせるのもありがと思いますが、来るデザインによってはマージンとかバラバラだしうまく使い回せない事が多いのでこうなりました。

ともかくこれでInputButtonの間には10pxのスペースができました。

Atom は要素の中に対して、Molecule は要素の外、つまりマージンや位置情報など他要素と関連し合うものをはここで設定しましょう。と考えると Template に近いものがあります。

計算済みの値を生成してもいい

例えば上記のサンプルコードのskin={buttonSkin}辺りがそれで、input用に渡した値を元にbuttonの見た目を変更しています。propsで渡した値をskinに渡せるようにしているのでアダプタ的な動きが近いかもしれません。

要素間のレイアウトを整えるものなので、それら要素間の依存する関係の値は持っても混乱は少ないと考えてます。またここでも状態を持たないので、Molecule と Atom の区別はそれほどしなくてもいいかもしれません。

Functional Component で定義

実装する際は最低でも Functional Component として定義するといいかと思います。複数の要素を扱うようになってくるのでプロパティの更新がなければ再レンダリングしないなどの制御も必要になってくるかなと思います。

React と AtomicDesign - Organizm(有機体)編

すべて個人的な考えです。

Atom や Molecule (や Organizm)をよしなに組み合わせるための Organizm (有機体)

Molecule と同じように。Atom や Molecule そして Organizm 要素をよしなに組み合わせて配置できるようにします。

この層では中に Atom や Molecule そして Organizm を配置できます。

データを取ってきて小要素に分配する

この層では子要素(Molecule や Atom)にレンダリングに必要なデータを渡してあげる役割もあります。従って、この層は小要素は何を欲しているかすべて知っている必要があり、それを元にデータを取ってくる必要があります。

それは例えばre-ducksの考えを取り入れていてreduxを使っているプロジェクトであれば、useSelectorを使って直接持ってくることができます。

データを取ってくるのは Page じゃないのかみたいな考えもあると思いますがコンポーネントの大きさによっても変わってくるかなと思います。例えば、全体をぼんやり見ているだけじゃただの石だけど、近くでじっくり見るとキラキラしていて綺麗みたいな(耳をすませばのアレ)

もちろん Page でもデータを取ってきていいのですが、Page は Page で必要なものだけ、それ以外のじっくり見た時の追加情報はそれぞれ Organizm 層でも取ってきてもいいんじゃないかと思います。

これは Page 層の肥大化を避けるという目的も大きいです。それは、Page 層でのレンダリングの制御が複雑になる事や、データを細部にまで行き渡す為にかなりの階層に伝えていかなければならない事を避ける狙いがあります。

React と AtomicDesign - Template編

atomic - template

すべて個人的な考えです。

Template は 間取り

Template 層は間取りです。例えば台所で考えると「冷蔵庫はここに置こう」「テーブルはここで」みたいに事前に決め、実際に電化製品から冷蔵庫が届いたら決められた場所に置いてもらうだけです。ただ間取りを考える人と実際に運んでいる置いてもらう人は別人です。個人的に Template 層は配置する人が迷わなくても決められた所に置けるようにすることだと思います。

Organizm 以前との違いですが、個人的に Organism などは順番に埋まっていく、 Template は狙った位置にストンと入れるみたいなイメージの違いがあると思います。

Template は何で定義してもいい

ただ僕は TypeScript にしかないですが、abstract classを使う方法がお気に入りです。

abstract class DetailTemplate extends React.PureComponent {
  abstract Header(): JSX.Element;
  abstract Main(): JSX.Element;

  render() {
    return (
      <ColumnLayout>
        {/* 上部に固定されるOrganism要素 */}
        <StickyMenu>
          <this.Header />
        </StickyMenu>

        <this.Main />
      </ColumnLayout>
    )
  }
}

継承したコンポーネントでHeaderを定義すると、それは勝手にColumnLayoutStickyMenuの下に配置されレイアウトのマークアップを意識せずに良くなります。 また、これを継承した時にHeaderMainを定義しなければコンパイルエラーにできるので、定義し忘れ防止もできます。

この層はデータに依存することがあまりないようにできると思うので、狙った位置に置けるJSX.Elementを返すただの関数でもいいと思います。

React と AtomicDesign - Page編

すべて個人的な考えです。

Template を使って適切な位置に要素を配置する

例えば以下のDetailTemplateは Abstract Class で、HeaderMainは Abstract Property です。そしてそれらコンポーネントは、間取りはどんな感じか分からないですが、とりあえず適切な位置に配置されるようになっています。

class FooPageContents extends DetailTemplate {
  // H1 は Atom
  Header = () => <H1>タイトル</H1>;

  // Article は Organizm
  Main = () => <Article in="foo" />;
}

const FooPage = () => {  
  return <FooPageContents />;
}

様々な副作用を担当

例えば、<header>の中身を変更するとかもこの層の役目です。

const FooPage = () => {
  React.useEffect(() => {
    document.title = 'foo - site'
  }, []);
  
  return <FooPageContents />;
}

ちなみにNextJSならマークアップだけで済みます。

import Head from 'next/head';

const FooPage = () => {  
  return (
    <>
      <Head>
        <title>foo - site</title>
      </Head>
      <FooPageContents />
    </>
  );
}

Redux を使っているなら Observer や共通するイベントの処理はこの層で適切にdispatchすることで管理し、各 Organizm や Molecule でそれらを乱立しあまり負荷を掛けない作りにすると良いと思います。

import ObserverContext from '...';
// const ObserverContext = React.createContext();

const FooPage = () => {
  const observer = React.useMemo(() => {
    return new IntersectionObserver(entries => {
      /*...*/
    });
  }, []);
  
  React.useEffect(() => () => {
    observer.disconnect();
  });
  
  return (
    <ObserverContext.Provider value={observer}>
      <FooPageContents observer={observer} />
    </ObserverContext.Provider=>
  )
}

JSXの中にインラインスクリプトを埋め込む

インラインスクリプト

こんな感じのインラインスクリプトを用意します。以下はGoogleAnalyticsの埋め込みタグです。

<script async src="https://www.googletagmanager.com/gtag/js?id=あなたのid"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'あなたのid');
</script>

JSXに変換

dangerouslySetInnerHTMLattrに__html: 'インラインスクリプト'を持ったオブジェクトをセットします。

<script async src="https://www.googletagmanager.com/gtag/js?id=あなたのid" />
<script
  dangerouslySetInnerHTML={{
    __html: `
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', 'あなたのid');
    `
  }}
/>

dangerouslySetInnerHTML.__htmlは最初気持ち悪く感じるかもですが、こういうものなのでしょうがないです。
中身ないので/>にするのも忘れずに書き換えます。

Enzyme で .simulate() した時に子要素の props が更新されない問題

バージョンなど

{
  "devDependencies": {
    "enzyme": "^3.7.0",
    "jest": "^23.6.0"
  }
}

以下は2番目のItemをクリックすると、次の状態ではaria-selected={true}となる事を確認しようとしていたテストです。しかし、これは通りません。

const secondItem = mount(Component)
  .find(Item)
  .at(1);
secondItem.simulate('click');
expect(secondItem.prop('aria-selected')).toBeTruthy();

解決

チェックする時に再度wrapperから対象の要素を取得したものを使えば更新された状態のItemが取れます。こうすれば、通ります。

const secondItem = mount(Component)
  .find(Item)
  .at(1);
secondItem.simulate('click');
expect(
  wrapper
    .find(Item)
    .at(1)
    .prop('aria-selected')
).toBeTruthy();

おまけ

一応setTimeoutを使っても通るよおです。

const secondItem = mount(Component)
  .find(Item)
  .at(1);
secondItem.simulate('click');
setTimeout(() => {
  expect(
    wrapper
      .find(Item)
      .at(1)
      .prop('aria-selected')
  ).toBeTruthy();
}, 0);

SSR サーバーサイドレンダリング

react-domモジュールのreact-dom/serverが提供するrenderToStringを使うことで React 要素を静的レンダリングできます。

renderToStringにはReact.ReactElementをそのまま渡します。

const React = require('react');
const {renderToString} = require('react-dom/server');

console.log(
  renderToString(
    React.createElement(
      'div',
      undefined,
      React.createElement('span', undefined, 'React SSR!')
    )
  )
);

このスクリプトは以下の HTML を表示します。

<div data-reactroot="">
  <span>React SSR!</span>
</div>

このようにして生成した HTML を初回フロントで表示することで、 JavaScript を実行するより前に結果を表示できます。

リポジトリ

この内容はココからクローンして試すことができます。

react-dnd の使い方

準備

HOC なDragDropContextで親コンポーネントをラップするか、DragDropContextProviderを親コンポーネントマークアップに含める必要があります。ただし、DragDropContextProviderは、react >= 16.0.0かつreact-dnd >= 4.0.0なバージョンである必要があります。

またブラウザ上で動作させる場合react-dnd-html5-backendというパッケージも必要です。イベントハンドラーのようなものがまとまったもので、これを使って色々解決する感じのようです。react-dndreact-dnd-html5-backendは基本同じバージョンを使うようにしましょう。

ルートとしては以下のようになります。

DragDropContextProvider

import React from 'react';
import {DragDropContextProvider} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

class App extends React.Component {
  render() {
    return (
      <DragDropContextProvider backend={HTML5Backend}>
        <div>なんでもいい</div>
      </DragDropContextProvider>
    );
  }
}

export default App;

DragDropContext

import React from 'react';
import {DragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

class App extends React.Component {
  render() {
    return <div>なんでもいい</div>;
  }
}

export default DragDropContext(HTML5Backend)(App);

少し分かりづらいですが、this.propsの値などは増えたりしないので注意です。

Drag and Drop (dnd)

Drag イベントを設定したいならDragSource、 Drop イベントを設定したいならDropTargetHOCを使います。どちらもreact-dndから提供されています。

共通項目

どちらとも第1引数にはtypeを指定します。これは処理をつなげる為に使われます。Fooというタイプの Draggable 要素をFooというタイプの Droppable 要素へドロップした時に初めてドラッグからドロップという一連の処理が実行されます。

第3引数のconnectreact-reduxconnectと同じような動きです。react-dndで管理している状態(state)をコンポーネントのpropsへ渡す為に使います。

第2引数のspecは関数をまとめたオブジェクトを渡します。これらのキー名はreact-dndが指定している名前にする必要があり、その一部は必須項目になっています。

react-dndの関数の引数にあるpropsは HOC の対象となるコンポーネントのpropsの値、monitorsは、react-dnd.github.io/r/d/a/drag-source-monitorにある内容がオブジェクトになった形のものが入っています。
propsなどは対象先ですが、monitorsには対象元が入っているような感じです。

DragSource

specには、beginDrag(props, monitor, component), endDrag(props, monitor, component), canDrag(props, monitor), isDragging(props, monitor)が設定でき、beginDragだけは必須項目です。それぞれ、

beginDragはドラッグが始まったタイミングに発火し、endDragはドラッグが終わったタイミングに発火します。beginDragendDrag戻り値は、monitor.getItem()で取り出せるようになります。

canDragはドラッグできるかの動的制御に使います。

HOC の対象となるコンポーネントでは最小限、connectconnect.dragSource()を受け取り Drap 対象とするJSX.Elementをその関数でラップする必要があります。

import {DragSource} from 'react-dnd';

DragSource(
  'BLOCK',
  {
    beginDrag(props) {
      console.log('beginDrag');
      return props;
    }
  },
  connect => {
    return {
      connectDragSource: connect.dragSource()
    };
  }
)(
  class extends React.Component {
    render() {
      return this.props.connectDargSource(
        <div style={{style: 'orange', width: 100, height: 100}} />
      );
    }
  }
);

DropTarget

先程 Drop 側のtypeBLOCKで指定したのでコチラ側もBLOCKを指定します。

こちらのspecは、drop(props, monitor, component), hover(props, monitor, component), canDrop(props, monitor)となっており、canDropは概ねcanDragのものと同じです。
こちらには必須となっている項目はありません。

dropは Dragable 要素を Droppable 要素の上で話した時に実行され、hoverは重なっている間連続で実行されます。

またこちらでは最小限、connectconnect.dropTarget(),を受け取り Drop 対象とするJSX.Elementをその関数でラップする必要があります。

import {DropTarget} from 'react-dnd';

DropTarget(
  'BLOCK',
  {
    drop(props) {
      console.log('drop');
      // onDrop は `<Foo onDrop={() => {}} />`
      // のように渡したもの
      // props.onDrop();
    }
  },
  connect => {
    return {
      connectDrogTarget: connect.dropTarget()
    };
  }
)(
  class extends React.Component {
    render() {
      return this.props.connectDrogTarget(
        <div style={{style: 'orange', width: 100, height: 100}} />
      );
    }
  }
);

Drag も Drop もできる要素

connect.dragSource()connect.drogTarget()2つでラップしてあげればいけます。

const Draggable = DragSource(/* ... */);
const Droppable = DropTarget(/* ... */);

const DNDCompoennt = Droppable(
  Droppable(
    class extends React.Component {
      render() {
        const {connectDrogTarget, connectDragSource} = this.props;

        return connectDropTarget(
          connectDragSource(
            <div style={{style: 'orange', width: 100, height: 100}} />
          )
        );
      }
    }
  )
);

仕上げ

DragDropContextProviderより深い部分かDragDropContextを適用したコンポーネント以下の部分で使うだけです。

class App extends React.Component {
  render() {
    return (
      <DragDropContextProvider backend={HTML5Backend}>
        <DNDComponent
          onDrop={() => {
            console.log('drop');
          }}
        />
      </DragDropContextProvider>
    );
  }
}

react-transition-group の使い方

react-transition-group@^2で提供されているコンポーネントは

  • Transition
  • CSSTransition
  • TransitionGroup

の3つです。

Transition

Transitionは以下のようなプロパティを取ります。

childrenには(status: 'entering' | 'entered' | 'exiting' | 'exited') => JSX.Elementが渡されます。このstatusの状態を使ってクラス名や何かしらの属性を内部の要素に設定し、CSSなどを使ってアニメーションさせることができます。

inは内部コンポーネントを表示するかどうかです。infalseの時はexitingからexitedに向かい、trueの時はenteringからenteredに向かいます。1番最初は単に-ingなしでenteredexitedが直接設定されます。

timeoutは、status-ingである状態の時間(msec)を指定します。これはaddEndListenerを設定しない場合は必須になります。
addEndListenerは動的にtimeout時間を設定したい場合に使えます。例えば CSS のtransitionでアニメーションするような場合、その終わりにtransitionendイベントが発火しますが、その発火するまでの時間を使うことでtimeoutの代わりとすることができます。(修正箇所も減る)これは第一引数にchildren関数での戻り値の要素が渡り、第二引数では-ingの終わりということになるdone関数が渡ってきます。この渡ってきたルート要素からアニメーションする要素を取得し、transitionendイベントでdoneを読んで、-ingを終えるといったことができます。注意点として、このaddEndListenerinが変わるたびに毎回実行されます。そのためイベントの登録などをしている場合、実行回数分登録することになってしまうので、毎回doneを呼ぶタイミングでイベントの解除模するといいと思います。

mountOnEnterintrueになるまでマウントしなかったり、unmountOnExitexitedになる度アンマウントする為のプロパティです。消える時はアニメーションしなくていいみたいな場合に使うといい感じです。

またonEnteredonExitedなどを渡して完了した時に連動してある処理を行うこともできます。ほかに、

  • 直前の onEnter, onExit
  • -ingonEntering, onExiting

があります。

CSSTransition

内部的にはある程度設定済みのTransitionです。与えられたTransitionと同様のintimeoutやこのコンポーネント独自のclassNamesを元に主により高機能なクラス名の管理を行ってくれます。与えたclassNamesにサフィックスとして-enter, -enter-active, -enter-done, -exit, -exit-active, -exit-done, -appear, -appear-activeがルート要素に付くようになるので、それらのクラス名を使って CSS でアニメーションを設定します。

サフィックスの名前は以下のような状態だと思うといいかなと思います。

  • -enter-exitTransition-ingが付く直前に付き、-doneが付く時取り除かれる
    • またはonEnteronExitは呼ばれたタイミングからonEnteredonExitedが呼ばれるタイミングまで
  • -enter-active-exit-activeTransition-ingの別名
  • -enter-done-exit-doneTransition-edの別名

TransitionGroup

1つ1つがTransitionを持つような配列要素をレンダリングしたい時にそれらをラップするように使うだけです。注意点は2つあります。まず、各配列のルート要素はTransitionである必要があります。2つ目はTransitionにはinではなくkeyが必須です。またこれはユニークな値である必要があります。inTransitionGroupkeyから有無を判定して自動で設定してくれるので考えないで済みます。

import {TransitionGroup} from 'react-transition-group';

(() => {
  <TransitionGroup>
    {items.map(item => {
      return 
        <CSSTransition key={item.id}>{
          status => {
            /* ... */
          }
        }</CSSTransition>
      );
    })
  </TranitionGroup>
})();

svgr の使い方

svgr.svgファイルから React コンポーネントに楽に変換できる CLI ツール(モジュール)です。

使うには実行ファイルのインストールが必要です。

yarn add -D @svgr/cli

Twitter ロゴの SVG を変換

こんな<svg>twitter.svgという名前で用意しました。

<svg
  aria-hidden="true"
  data-prefix="fab"
  data-icon="twitter"
  class="svg-inline--fa fa-twitter fa-w-16"
  role="img"
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 512 512"
>
  <path
    fill="currentColor"
    d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
  ></path></svg
>

このファイルに対して変換を行ってみます。

2つのオプションがついています。 --icon<svg>width:1emheight:1emを付けるオプションで、--replace-attr-valuesはマッチした属性値を渡した値で上書きするオプションです。

npx @svgr/cli --icon --replace-attr-values "currentColor=#46a1eb" twitter.svg

# ファイルに出力
# yarn add -D @svgr/cli
# yarn --silent svgr --icon --replace-attr-values "currentColor=#46a1eb" twitter.svg > svg-twitter.svg

以下のような結果になりました。

import React from "react";

const SvgTwitter = props => (
  <svg
    aria-hidden="true"
    data-prefix="fab"
    data-icon="twitter"
    className="twitter_svg__svg-inline--fa twitter_svg__fa-twitter twitter_svg__fa-w-16"
    viewBox="0 0 512 512"
    width="1em"
    height="1em"
    {...props}
  >
    <path
      fill="#fff"
      d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"
    />
  </svg>
);

svg の中身を React 内で触りたい場合

例えば Snapsvg などで SVG の中の要素を触りたい時に必要かなという設定です。

svgrコマンドをそのまま使うとsvgoによる最適化が行われてしまい、望んだ形で変換できない時があるかもしれません。その場合は.svgo.ymlという設定ファイルをプロジェクトルートへ置き、設定を書くことで直すことができるかもしれません。

一部タグがおかしい

デフォルトでは<polygon points="...">といったタグは<path d="...">に変換されます。これを防ぐには、convertShapeToPathを切ります。

plugins:
  - convertShapeToPath: false

一部タグが消える

僕が遭遇したものだと、<clip-path>が消えました。これは以下の2行を設定することで消されることを防げました。

plugins:
  - cleanupIDs: false
  - removeUnknownsAndDefaults: false

TypeScript で出力した .jsx を読めるように

以下のようなファイルをaaa.jsxであれば、aaa.d.tsとなるように作成し、SVGComponentexportしている名前に変換します。

import React from 'react';

declare const SVGComponent: React.SFC;
export default SVGComponent;

これでimport Aaa from './aaa';などで.tsxに読み込めます。

react-testing-library

コードは Jest 環境のものを想定してます。

コンポーネントレンダリング

@testing-library/reactからrenderをインポートしてきて使います。大体はこの関数によりレンダリングしてから色々テストすることになります。

import {render} from '@testing-library/react';

const Button = ({children}) => (
  <button>
    <span>{children}</span>
  </button>
);

test('...', () => {
  const renderResult = render(<Button>foo</Button>);
  // ...
});

renderRenderResultを返しますが、これは色々なものが詰まったオブジェクトです。

debug

debugはどんな DOM 構造でレンダリングされたか確認したい時に便利なものです。

renderResult.debug();
// <body>
//   <div>
//     <button>
//       <span>foo</span>
//     </button>
//   </div>
// </body>

引数としてbaseElementが渡せます。これを渡すと表示する DOM の範囲を狭めれます。

renderResult.debug(renderResult.container);
// <div>
//   <button>
//     <span>foo</span>
//   </button>
// </div>

container

レンダリングする要素の入れ物となるコンポーネントです。デフォルトでは<div>で囲まれます。(つまり、div > button > spanの構造でレンダリングされる)
入れ物はrender時にオプションを指定することで変更可能です。

const article = document.createElement("article");

render(<Button>foo</Button>, {
  container: document.body.appendChild(article)
}).debug();
// <article>
//   <button>
//     <span>foo</span>
//   </button>
//</article>

Queries

要素を取得するための様々なヘルパーです。get.*find.*query.*から始まるメソッドが多数用意されてます。これらの違いはgetfindは必ず要素が無ければ駄目で、queryは無くても良い点大きく違う点です。
また、Allと付くヘルパー(例えばgetAllBy.*)は単体ではなく配列で返すようになります。

Queries はByまたはAllByの後ろに決められたサフィックス(手段名)を付けることで、その手段で要素を取得します。検索文字は絶対マッチでも良いですが、/foo/iのような正規表現でもマッチさせれます。

手段には以下のようなものがあります。
LabelTextは以下のようなケースの<label>を取得します。

  1. label > input
  2. label[aria-label]
  3. label[for] + input[id]
  4. label[id] + input[aria-labelledby]

PlaceholderTextplaceholder属性を持つ要素を取得します。

  1. [placeholder]

Textはそのテキストを持ってる要素を取得します。

  1. {text}

AltTextalt属性を持つ要素を取得します。

  1. [alt]

Titletitle属性を持つ要素か SVG のタイトルとマッチした要素を取得します。

  1. [title]
  2. svg > title

DisplayValueは入力系の要素に現在入ってる値にマッチした要素を取得します。

  1. input
  2. textarea
  3. select > option[selected]

Roleは要素の役割にマッチした要素を取得します。これは HTML によってデフォルトで割り振られた Role にもマッチします。
デフォルトロールはmomdo.github.io/html-aria/#document-conformance-requirements-for-use-of-aria-attributes-in-htmlが参考になります。

  1. [role]

TestIddata-testid属性を持つ要素を取得します。

  1. [data-testid]

rerender

rerenderはそのコンポーネントを再レンダリングできます。これは(下記の)fireEventなどでコンポーネント内部の状態が変わった時にそれを反映させれます。

以下はレンダリング後はテキスト要素があるが、再レンダリング後は無くなっている事をテストしている例です。

import React from 'react';
import {render, fireEvent} from '@testing-library/react';

const Button = ({children}) => {
  const [hide, setHide] = React.useState(false);

  return (
    <button onClick={() => setHide(true)}>
      {!hide && <span>{children}</span>}
    </button>
  );
};

test('hide text when has clicked', () => {
  const renderResult = render(<Button>foo</Button>);

  const button = renderResult.getByRole('button');

  expect(renderResult.getByText('foo')).toBeTruthy();

  fireEvent.click(button);
  renderResult.rerender();

  expect(() => renderResult.getByText('foo')).toThrow();
});

unmount

unmountはコンポーネントを JSDOM 環境から取り除きます。例えばコンポーネントが内部で外部の DOM を弄っている場合、その後処理がちゃんとできているかなどの確認に使えます。

イベント発火

fireEventでは Queries などから取得した要素を引数にイベントを Dispatch できます。

import {fireEvent} from '@testing-library/react';

// ...

fireEvent.click(getByRoke('button'));

エラー

Cannot read property 'createElement' of undefined

TypeScript 環境で置きている場合、tsconfig.jsonesModuleInteropが未設定または、false設定の時に、

import React from 'react';

のように読み込んでると起こります。esModuleInteropがオフの場合以下のように読み込むのが正解です。

import * as React from 'react';

React は UMD で定義されていてその中身は、

module.exports = {
  createElement: /* ... */,
  /* ... */,
}

のようだと定義されてます。これでesModuleInteropをオンにすると、この中身がすべてdefaultからも強制的にエクスポートされるようになります。

module.exports = {
  default: {
    createElement: /* ... */,
    /* ... */,
  },
  createElement: /* ... */,
  /* ... */,
}

import <name> from <module><name>defaultを見てるので、esModuleInteropがオフの場合はこれが無くエラーとなるのだと思います。