Cycle.jsでReactのPortalっぽいことをしたい
気付いたらクリスマスの夜なんですけどどういうことですか(困惑)。最近クリスマス = 平日なので一切特別感がなかったんですが、久々に休日がクリスマスになって特別感ありますね?(疑問) ちょっとCycle.jsと長い戦いを繰り広げていますが、その中で一個やりたいことが多分できたので、軽いネタとして紹介します。 Portalとは? もしかしたらPortalってなによ?という方もいらっしゃるかもしれないので紹介しておきます。私が知る限りこいつを一般的に広めたのはReact.jsだと思います。 さっと探してみたりはしたんですが、2017年くらいのStackoverflowでReactのportalの話題があるくらいなので、コンポーネントライブラリとしてこういうのを追加したのはReact.js・・・で合ってるのかな? https://reactjs.org/docs/portals.html#gatsby-focus-wrapper 詳しくは上記の公式ドキュメントが詳しいですが、要はよくあるモーダルダイアログ、つまりは 全体の上に表示されないといけないもの を実装するということをシンプルにしてくれるやつです。上記のサンプルがClass baseなコンポーネントなのは結構古いままなんだろうか? Portalが使えるうれしさとは 例えばですが、↓みたいな構造のHTMLになってて、モーダルは #modal-root ってことに出したいよ・・・っていうことを考えます。 <html> <body> <div id="root"> <!-- ここがアプリケーションのルート --> </div> <div id="modal-root"> <!-- ここにモーダルとか出したい --> </div> </body> </html> よくある、フッターをクリックしたらデザインされたconfirmationを出したい・・・ってのがありますが、React.jsで普通にそれをやろうとすると、結構しんどいです。また、モーダルも大抵はコンポーネントとして作成すると思いますが、それらのモーダルに対してpropsを渡すとき、基本的には 表示するという主体を実行したいコンポーネントの中 にモーダルを宣言したくなります。が、普通にやると、それをやってしまうと overflow:hidden とか諸々にひっかかって悲しいことになることがよくあります。 Portalは、 Componentみたいに扱えるけど、Componentとか違うところにレンダリングする ということそのものをやってくれます。この恩恵はReact.jsにおいては結構大きいと思います。これがないと、外部のstateを利用しない限り、こういった出しわけができないですし。最近のReactが強烈に推し進めている、Functional Componentの上でも重要な機能だと思います。 Cycle.jsでPortalを作ってみる Cycle.jsでは、副作用はdriverとして 扱わなければならない というのがルールです。PortalはDOMを扱うので、自然とdriverになります。 とりあえずの実装は以下のようになりました。解説は後で。 import { Driver } from "@cycle/run"; import { Stream } from "xstream"; import { VNode } from "snabbdom"; import { IsolateableSource } from "@cycle/isolate"; import { div, MainDOMSource, makeDOMDriver } from "@cycle/dom"; // Portalが受け取る = コンポーネントから渡されるSink export interface PortalSink { [k: string]: VNode; } // Portalが返す = コンポーネントが受け取るSource // isolateで諸々区別したいので、IsolateableSourceを継承している export interface PortalSource extends IsolateableSource { DOM: MainDOMSource; isolateSource(source: PortalSource, scope: string): PortalSource; isolateSink(sink: Stream<PortalSink>, scope: string): Stream<PortalSink>; } // Sourceの実装 class PortalSourceImpl implements PortalSource { public DOM: MainDOMSource; constructor(private _rootDOM: MainDOMSource, private _rootSelector: string, private scopes: string[]) { this.