Reactコンポーネントを単独で使うための細かいテク

前回のReactコンポーネントを単独で使うに書いたようにしばらくやってみて、細かいところのいい感じのやり方がわかってきた。

ディレクトリ構成

└── src/
    ├── components/
    │   ├── react/
    │   │   ├── AwesomeApp.js
    │   │   └── Disclosure.js
    │   ├── AwesomeApp.js
    │   ├── Disclosure.js
    │   └── GlobalNavigation.js
    └── main.js

初期化を担うファイルはcomponents/の直下に、Reactコンポーネントcomponents/react/に配置する。Reactコンポーネントを初期化する処理は、components/にファイルを作成した上でそこに書く。Reactじゃないコンポーネントも同じディレクトリに配置する。

stateを制御するコンポーネントでラップする

プレゼンテーショナルなReactコンポーネントには、トップダウンで状態を渡すことが多い。

const Disclosure = ({ isExpanded, onToggle }) => (
  <div className="Disclosure">
    <button
      className="Disclosure__toggle"
      type="button"
      aria-expanded={String(isExpanded)}
      onClick={() => onToggle(!isExpanded)}
    >
      toggle
    </button>
    <div
      className="Disclosure__content"
      hidden={!isExpanded}
    >
      {children}
    </div>
  </div>
)

これを単独で機能させるというのをやりやすくするために、簡単にstateを管理できるコンポーネントを作る。あるいはreact-valueがちょうど良い。

import React from 'react'
import ReactDOM from 'react-dom'
import { Value } from 'react-value'
import Disclosure from './react/Disclosure'

export const init = () => {
  document.querySelectorAll('.react-Disclosure').forEach((containerEl) => {
    const initialExpanded = containerEl.dataset.open === 'true'
    ReactDOM.render(
      <Value
        defaultValue={initialExpanded}
        render={(value, onChange) => (
          <Disclosure
            isExpanded={value}
            onToggle={onChange}
          />
        )}
      />
      containerEl,
    )
  })
}

クラス名の命名規則

ReactコンポーネントReactDOM.render()で描画する以上、マウントする対象の要素が必要になる。そのため、単独で利用するReactコンポーネントには必ずラッパー要素ができる。

コンポーネントのクラス名が.Disclosureの場合、そのラッパー要素のクラス名は.react-Disclosureにする。このラッパー要素は、利用方法によって存在したりしなかったりするので、あってもなくても同じ振る舞いをするように実装する。

ラッパー要素のスタイル宣言は、内包するコンポーネントと同じファイルに記述する。

_Disclosure.scss:

.react-Disclosure {
  ...
}

.Disclosure {
  ...
}

data-*属性をコンポーネントに渡すpropsとして利用する

上記の例でもそうしたけど、ラッパー要素のdata-*属性にReactコンポーネントへ渡すpropsを設定しておくと便利。

<div class="react-Disclosure" data-open="true"></div>

propsへHTMLを渡したいときは、例えばPugなら次のようにすればいい。

div.react-Disclosure(hidden)
  ul
    li foo
    li bar

innerHTMLをpropsに渡しつつ、描画が終わったらhidden属性を取り除く。あまりきれいじゃないけど。

配列やオブジェクトを渡したいときは次のようにする。

- const array = JSON.stringify(['foo', 'bar', 'baz'])
- const object = JSON.stringify({k: 'val'})
div.react-MyComponent(data-array=array data-object=object)

data-*属性の値をJSON.parse()してpropsに渡す。無駄感はあるけど、テンプレートの管理の楽さとかを考えるとまあこれでいいかなという感想。