React とGUI 設計論、あるいは新世代のホームページビルダー

注意。実装はまだないです。思考実験的な意味合いが強いです。

持論

Reactやredux/Rxのデータモデリング手法の発達で、ツリー構造の末端に渡すまでのデータモデリングが主戦場になりつつあります。これはロジックを注入する部分と、プレゼンテーショナルなものが明確に分離されてきたことを意味します。

僕は個人的に、 GUI にまつわるものは、本来GUIで設計したい、という気持ちがあります。そう、僕が作りたいと思っているのは、悪名高きホームページビルダーです。

とはいえ、プログラミング抜きでxxxできる!というものではありません。むしろプログラミングとGUIを横断するイメージで、Unity や UnrealEngine のような開発環境を想定しています。

今やりたい理由

  • ブラウザの仕様が安定してきた
  • 色々と使えるパーツが増えた
  • JS で複雑なツールを作れるようになり、インブラウザな開発ツールが作りやすくなった

開発環境の中で自分自身を実装する、という Smalltalk っぽい開発環境を作りたい、という話です。

使えるパーツ CSS Grid Layout

Flux にはコンテナーコンポーネントという概念があります。これはViewとしての表象を持たず、ストアから紐付けられるデータだけを子コンポーネントに渡すメタコンポーネントです。

同じように、CSS でも、レイアウト用のコンポーネントと、プレゼンテーショナルなコンポーネントを分類しよう、という考え方があります。

これからのCSSはmargin禁止!?CSSグリッドレイアウトやコンポーネント指向なCSSについて、矢倉さんに聞いてきた!

React で GridLayout をちょっと使ってみました。

/* @flow */
import React, { Fragment } from 'react'
const Header = () => <Fragment>Header</Fragment>
const Content = () => <Fragment>Content</Fragment>
const Footer = () => <Fragment>Footer</Fragment>
const Menu = () => <Fragment>Menu</Fragment>
export default function Layout() {
  return (
    <Fragment>
      <div style={styles.gridContainer}>
        <div style={styles.header}>
          <Header />
        </div>
        <div style={styles.content}>
          <Content />
        </div>
        <div style={styles.menu}>
          <Menu />
        </div>
        <div style={styles.footer}>
          <Footer />
        </div>
      </div>
    </Fragment>
  )
}

const styles = {
  gridContainer: {
    width: '800px',
    height: '600px',
    display: 'grid',
    gridTemplateColumns: `1fr 2fr 2fr`,
    gridTemplateRows: `
      50px
      1fr
      50px
    `,
    gridTemplateAreas: `
        "menu header  header"
        "menu content content"
        "menu footer  footer"
    `
  },
  header: {
    gridArea: 'header',
    backgroundColor: 'rgb(100, 200, 200)'
  },
  menu: {
    gridArea: 'menu',
    backgroundColor: 'rgb(100, 100, 200)'
  },
  content: {
    gridArea: 'content',
    backgroundColor: 'rgb(230, 255, 230)'
  },
  footer: {
    gridArea: 'footer',
    backgroundColor: 'rgb(200, 200, 200)'
  }
}

こんな感じ。

プログラマブルでわかりやすいですね。少なくとも僕はそう思います。

使えるパーツ React.Fragment

上記の手法をやるには、実は React v15 まではやや難しいところがありました。
React v15 までのコンポーネント志向の大きな問題の1つは、必ず要素が1つのルート要素を持たないといけない、という縛りです。

function App () {
  return <div>
    <span>a</span>
    <span>b</span>
  </div>
}

たとえ描画したい要素が span だけだとしても、親要素を1つ取る必要があり、この div の影響を排除するために、一旦 { width: 100%; height: 100%; } をつけて回るなどして回避するなどのバッドノウハウが必要になることが、よくありました。

React v16 からはこう書けます。この辺り renderer が全面的に書き直されたことの成果の1つです。

function App () {
  return <>
    <span>a</span>
    <span>b</span>
  </>
}

または

import { Fragment } from 'react'
function App () {
  return <Fragment>
    <span>a</span>
    <span>b</span>
  </Fragment>
}

前者は次の babel のバージョンからサポートされ、後者は今でも使えるバージョンですね。

かならず親の空要素が必要になる、というのは、とくに react と react-native で共通のフレームワークを作るときなどに邪魔でした。react だと div でも react-native では View ですからね。

というわけで、コードを空で実装したいときは、Fragment でやればよくなりました。これで親子関係を考えるときや、データモデリングとプレゼンテーションを考えるのを後回しできます。

UI実装の分類

これを組み合わせると、実装すべきコンポーネントが以下のように分類できると思います。

  • Tree: 概念的な親子構造を規定するコンポーネント
  • Layout: ルート要素として配置を規定するコンポーネント
  • Selector: ロジック注入だけのコンポーネント
  • View: プレゼンテーショナルなコンポーネント

今勝手に名前をつけました。

実装的にはこうです。

  • Tree: Fragment で他の層をラップ
  • Layout: children を 配置する Grid Layout 定義層
  • Selector: ReactRedux.connect
  • View: Stateless Component

で、User と Home というよくある画面を定義してみるとこうなります。

const RootTree = () =>
  <RootLayout
    header={() => <Selector><Header/></Selector>}
    footer={() => <Footer/>}
    content={
      <RoutingSelector
        Home={() =>
          <HomeTree>
            <HomeSelector>
              <HomeLayout>
                <HomeView/>
              </HomeLayout>
            </HomeSelector>
          </HomeTree>        
        }
        User={() =>
          <UserTree>
            <UserView/>
          </UserTree>
        }
      />
    }
  />

すごい適当です。適当に捉えてください。ルータもいわゆるSelector の一種だと解釈してます。この中に画面スイッチの実装があるというイメージです。

ここでは Atomic Design みたいな文脈はまだ出てきません。それが出てくるのはプレゼンテーショナルな View の中の分割指針としてです。

こう考えてみると、 Tree => Selector => Layout => View | Tree という 大きなパターンを繰り返してることがわかりますね。

実装に落とし込んでいく

このツリーをそもそもyaml定義するなりAST解析なりで分析したとします。大変そうだけど、今は概念的には実装できるよね、という前提で進めます。

これはまだ抽象的な状態ツリーなので、ファイル名ベースでマッピングすることで実装します。
src/components/**/*.js みたいな感じでglobして名前空間を繋ぐイメージ。

src/components/HomeView.js
export default () => <div> This is HomeView. </div>

これは頑張って人間が実装するしかないですね。あるいは本当にUIパーツを作るホームページビルダー的なものを作るならここがターゲットです。iOSのStoryboard的なやつ。

Layout の部分はこんな感じでしょうか。

src/components/RootLayout.js
export default ({header: Header, footer: Footer, content: Content}) => {
  <div style={styles.gridContainer}>
    <div style={styles.header}><User/></div>
    <div style={styles.content}><Content/></div>
    <div style={styles.footer}><Footer/></div>
  </div>
}

これはGUIエディタ作って自動化できそう。

というわけで途中まで作りました。

…いや、全然実装途中です。これに各種の子Component定義をマッピングするイメージ。

書きかけのコードはここ https://gist.github.com/mizchi/e0412f8a70ff4f1e5b01a48ddb7c039d (なんか違うペーンと接続してしまうバグが直せない)

追記

もうちょっとちゃんと作った

ここで試せます

https://mizchi-sandbox.github.io/grid-generator/

行や列の削除とかはまだ。

HoC 積む部分のGUI化

今のReactを、ある程度高度に HOC 含めて使っていくと、こんな感じのコードになっていくと思います。

import { compose, pure } from 'recopomose'
import { connect } from 'react-redux'
import withLayout from './hoc/withLayout'

export default compose(
  compose(state => state.users),
  withLayout,
  pure
)(props => {
  return ...
})

compose で HOC を合成します。というわけで、↑のエディタとして、Container に対して 積む hoc を指定する、ということができないか?というのを考えています。
UIとしては、Unity の右側の編集ペーンみたいなイメージです。

hoc:
  selector: [fooSelector] //=> src/selectors/fooSelector.js
  [withLayout]
  [recomponse.pure]
  [+ add hoc]
children: [Foo] //=> src/components/Foo.js

この方式でやると、fooSlecetor と Foo の型を紐付けることができ、TS/Flowでそういうコードを生成すれば、型付まで自動でできるようになりますね。

compose で mapStateToProps は 最近だと selector などと呼ばれることがあり、https://github.com/reactjs/reselect などを使うとメモ化しつつ効率的な state 取り出しを行うことが可能です。

また、その component が発火しうる action も列挙できそうです。

  withActions:
  - [ ToggleHeader ]
  - [ ToggleSidebar ]

このアクションはコードを静的解析して列挙すれば取れるはず。

再度ツリー構造を考える

withLayout みたいなHOCで自分自身のレイアウト構造を、自分で指定できることを考えると、

Tree => Selector => Layout => View | Tree

Tree => Container<HOC[]> => Tree | View

みたいな構造として、HOCの実装をたくさん積むことで振る舞いを定義したほうがよさそう。
末端の View にたどり着いた時点で、React.Component として自分でコードを書き直すし、その中で、別途 Tree を子にとることもある、みたいなイメージ。

このデメリットとして、 実装上、 connect が頻出してパフォーマンスが悪くなる気がしていて、そこは reselect に更新を間引かせるなどして解決する必要があります。

考察

グリッドレイアウトによってレイアウト配置、その子コンポーネントのHOCを指定するところまでGUI環境で自動化できそうな気がします。

逆に、その中の細かい実装までは担保するのが難しそうだし、そもそもやるべきではない。やりたければホームページビルダーを実装する、みたいな話になりそうです。

(余談ですが、この辺、Sketch でReact Component 吐き出しとかできないんですかねー。svg 直埋め込みでモックは作れそうだし、そういうモック実装ならみかけたんですが…)

この開発環境は react-storybook 上に展開して、開発モードでペチペチとレイアウト配置と対応コンポーネントをマッピングして、なんとかしてファイルを書き込んで(electron上で実装するとか)、その末端である component に props を確定させて渡す、その先は自分で実装する、みたいなワークフローになりそうです。

その場合は、末端はこんなコードになるのかも

implementFor('UserView', (props) => {
  // props は HOC 積まれて解決された状態
  return ...
})

そういえば @angular/elements みてたら webcomponents として相互に行き来できるようになりそうなんで、ここから末端のElementはそもそもReactである必要すらないのかもしれません。

https://qiita.com/studioTeaTwo/items/13d3eed751781bf039fc

Action定義もGUI化できないか

Action が発火されてから、 middleware を通って再度 reducer にたどり着くまでを、パイプライン的に表現できる気がしています。

Unreal Engine のブループリントみたいなイメージです

https://docs.unrealengine.com/latest/JPN/Engine/Blueprints/index.html

ただし、これはよっぽどうまくやらないと、ごちゃごちゃして破綻する、というのを UE勢がいってるのをTwitterで見たので、よっぽど上手く作れるという確信がもてるまで、下手に手を出さないほうが良さそう。

まとめ

  • React.Fragment/GridLayout でレイアウトのツリー定義と、ロジックのマッピングまでGUIでポチポチ実装できそう。
  • 実装はない。時間が出来たら作る。