2019-06-10: react-cytoscape-renderer反省会
やったこと
fruity love最終日
色々あって166000ptでfinish。
https://twitter.com/make_now_just/status/1138052111785029633
若干微妙な気もするけど、多分大丈夫だと思う。タブンネ‥‥。
react-cytoscape-renderer反省会
やっぱ無理だという結論になったcytoscape.jsのReactレンダラを作るやつの反省会。
まず、何をやろうとしていたのか、という話から。
完成イメージとしてはこんな感じだった。
import React from 'react';
import ReactDOM from 'react-dom';
import {Cytoscape, Node, Edge} from 'react-cytoscape-renderer';
const stylesheet = [
{selector: ':compound', style: {'background-color': '#ddd'}},
{selector: 'node', style: {'background-color': '#666', 'label': 'data(id)'}},
{selector: 'edge', style: { 'width': 3, 'line-color': '#ccc', 'target-arrow-color': '#ccc', 'target-arrow-shape': 'triangle'}},
];
const Main = () => (
<Cytoscape stylesheet={stylesheet} style={{width: '800px', height: '600px'}}>
<Node data={{id: 'hello-world'}}>
<Node data={{id: 'hello'}} />
<Node data={{id: 'world'}} />
<Edge data={{source: 'hello', target: 'world'}} />
</Node>
</Cytoscape>
);
ReactDOM.render(<Main />, document.querySelector('#main'));
<Cytoscape>
コンポーネントの中にある<Node>
とか<Edge>
は通常のコンポーネントではなくて、<Cytoscape>
コンポーネント内でcytoscape.js用のレンダラで描画される、というのがポイント。
あとネストした<Node>
はcytoscape.jsのcompoundノードとして解釈される、というイメージ。
これをTypeScriptで実装してた。
ダメだったところ
はじめに何が失敗だったのまとめていく。
まずcytoscape.jsはノードとかエッジの生成だけをすることができなくて、必ず同時にグラフに追加しなきゃいけない、という制約があった。
これが地味に大きな問題点。
加えて、ノードとエッジの関係が結構厳しい。
id
が重複すると即座に例外が飛ぶ。id
を変更できない。- エッジの
source
とtarget
のノードはエッジを追加するときに存在しなければいけない。 - ノードを削除すると、そのノードにつながっているエッジも削除される。
とまあ、この辺りが面倒くさい。
id
の重複は、例えばあるノードを別のノードの子ノードとして移動した場合に、削除→追加の順で行われれば上手くいくのだけど、追加→削除の順になってしまうと同じid
のノードが一時的に複数存在してしまい例外が発生する。
エッジがsource
とtarget
のノードを要求するのも、エッジの追加処理が先に行われる場合に起こるので、ノードから追加されることを保証しなきゃいけなくて面倒。
とはいえ、ここまでの問題は追加・削除の操作をキューして、削除→追加の順で一律に行うようにすれば回避できる気がする。
もっと本質的な問題はid
が変更できないことと、ノードを削除するとつながっているエッジも削除されてしまうところだったりする。
id
が変更できないのは面倒。cytoscape.jsが返すオブジェクトを適当にラップしておいて、id
を変更しなきゃいけないときは今の要素を新しいid
で上書きしてコピーしてから削除する、みたいなことをすればどうにかなるかもしれない。
ノードを削除するとつながっているエッジも削除されてしまうのは、ノードだけを削除すると、Reactの仮想DOM上には存在するのに、実際には存在しないエッジが生まれてしまうのが面倒。多分、そういったノードが生まれてしまったらエラーにしなきゃいけないと思うのだけど、チェックするのが大変な気がする。
という感じ。特に最後のやつがどうしようもない気がしたので諦めた。
根本的な問題は、ノードの親子関係はツリー構造だけど、エッジはツリー構造ではない、というところな気がする。難しい。
良かったところ・分かったところ
とはいえ、コンセプトはそんなに悪くなかったんじゃないかと思っている。
今回やってみて色々分かった部分もあるので、その辺りをまとめていく。
良かったところ;
- compoundなノードの関係をノードのネストで表現できたのは良いところだと思う。反面、ノードの中のエッジなどに意味が無くなってしまったのはよくなかった。
- ノードやエッジに直接イベントを設定できるところ。普通に分かりやすくて良い。ただ、グラフの可視化という目的だと使わないかも、
分かったこと:
- Reactのレンダラの書き方。意外とやってみれば書ける。
- TypeScriptでReactのレンダラを書く技術。
HostConfig
という型が重要なのだけど、型引数がいっぱいあって分かりづらい。何となく名前で分かる。- React本体の型はしっかりしてるけど、Reconciler周りは怪しい部分もある。オプショナルなはずの引数が必須扱いだったり——。そんなに困らないけど。
- TypeScriptでcytoscape.jsを書く技術。
- 型定義が全体的に足りてなかったり間違ってしたりしてつらい。気が向いたらコントリビュートしたい。
- その他、Reactの仮想DOMを使ってツリーっぽい構造を制御する方法。今回は使ってないけど、react-zdogみたいにコンポーネントのライフサイクルで副作用を起こして管理する、というのも面白い。
思うところをまとめるのは大事。なんかReactのレンダラ作りたいな。