• ..

git

    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 と AtomicDesign - Atom編

    HTML 要素と同じように < と > で囲んで使う要素は Atom

    <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 と考えます。

    // 定義
    const createLoginForm = (fields: Field[]) => {
      const LoginForm = () => (
        <Form>
          {fields.map(field => {
            return <Input key={field.name} />;
          })}
        <Form>
      );
    
      LoginForm.displayName = 'LoginForm';
    
      return <LoginForm />;
    }

    例えば Molecule を使う時はこのように、単に変数埋め込み記法を使うだけです。

    class extends React.Component {
      render() {
        const loginForm = createLoginForm([/* ... */]);
    
        return (
          <Flex>
            {loginForm}
          </Flex>
        )
      }
    }

    Molecule の定義の仕方

    tbw

    React と AtomicDesign - Template編

    Template は Webページの間取り

    Templateは、間取りです。家で例えるならこの部屋はこういうことに使おう、あっちの部屋はああいうことに、と決めるような感じです。Webであれば、「header要素はこの中に」「メイン要素はこの中に」となると思います。

    そしてベッドなら寝室に置くでしょうし、ブログ記事ならメイン要素に大抵は置くだろうと思いますが、Template層はそういうブログ記事をレイアウトを意識せずにメイン要素に配置できるようにするのが役割です。

    Template は Abstract Class

    僕は、abstract classを使う方法がお気に入りです。

    abstract class DetailTemplate extends React.Component {
      abstract Header(): JSX.Element;
      abstract Main(): JSX.Element;
    
      render() {
        return (
          <ColumnLayout>
            {/* 上部に固定されるOrganism要素 */}
            <StickyMenu>
              <this.Header />
            </StickyMenu>
    
            <this.Main />
          </ColumnLayout>
        )
      }
    }

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

    Page層ではこのように継承して使います。

    class FooPage extends DetailTemplate {
      Header = () => <h1>タイトル</h1>;
    
      // `this.props.article` はreduxなどで渡ってきた値
      Main = () => <Article article={this.props.article} />;
    }

    ちょっとはシンプルになると思います。

    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);

    React の型周りでめちゃくちゃエラーがでるようになった

    もしかしたら色々な@types/reactのバージョンが混在しているからかもしれません。

    @types/react のバージョンが混在していないか調べる

    以下のコマンドを実行してみてください。

    yarn list --pattern @types/react

    もし2階層も3階層も表示される場合は混在しています。

    これを解決するにはプロジェクトルートで以下を実行します。

    rm -rf node_modules yarn.lock && yarn cache clean && yarn

    再度yarn listします。1階層になっていれば大丈夫だと思います。

    yarn list --pattern @types/react

    ただReactの昔のバージョンを使ってるときは、これでは駄目な気がします。その時が訪れたら追記するかもしれません。

    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 ツール(モジュール)です。

    使う準備

    とりあえず以下で bin のインストールが必要です。

    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に読み込めます。