React:props で Cytoscape.js の図を更新する試作

試作内容

<Child
  Elements={el}
  Styles={sy}
/>

上記のように、props で cytoscape へ設定するデータを渡して更新する Child コンポーネントの作成を目指します。

テンプレ作成

ベースとなるアプリは以下のコマンドで用意しました。

npx create-react-app my-app --template=typescript
cd my-app
yarn add cytoscape
yarn add -D @types/cytoscape

試作したコンポーネント

import React, { FC, useRef, useEffect } from 'react';
import cytoscape, { Core, ElementDefinition, Stylesheet } from 'cytoscape';

const Child: FC<{
  Elements?: ElementDefinition[],
  Styles?: Stylesheet[],
}> = ({
  Elements = undefined,
  Styles = undefined,
}) => {
    const refEl = useRef(null);
    const refCy = useRef<Core | null>(null);

    useEffect(() => {
      const container = refEl.current! as HTMLDivElement;
      refCy.current = cytoscape({
        container: container,
        elements: Elements,
        style: Styles,
      });
      const cy = refCy.current;
      return (() => {
        cy.destroy();
      })
    }, []);

    useEffect(() => {
      const cy = refCy.current;
      cy?.json({ elements: Elements, styles: Styles });
    }, [Elements, Styles]);

    return (
      <div ref={refEl} style={{ width: '300px', height: '200px' }}></div>
    );
  }

export default Child;

主なポイントは、useEffect を以下の2つに分けて処理するところでしょうか。

  • 初期に描画する処理(14行目以降)
  • 更新された props のデータで描画する処理(27行目以降)

14行目の useEffect の第2引数に Elements と Styles を入れても更新は可能でしたが、はじめからの描画になるので、図の変更(ノードの移動など)が戻ってしまいました。
その為、27行目の useEffect で props の更新に反応させ、

src/Child.tsx
  Line 25:8:  React Hook useEffect has missing dependencies: 'Elements' and 'Styles'. 
 Either include them or remove the dependency array  react-hooks/exhaustive-deps

と言う指摘は無視しています。

やはりこの手のライブラリ使用は useEffect でハマりそうな気がしますね。
あと、cytoscape の要素の初期の高さをどうするか悩みそうでした。
何もしないと高さがゼロになって何も描かれないんですよね。
仕方がないので今回は固定値を設定しました。

作成したコンポーネントを使用したスクリーンショット

ボタンクリックで、Node1 の色が変わります。

  • blank:黒
  • A:赤
  • B:青

その他のファイル

Child.tsx の他に、修正または追加したファイルは以下の通りです。

import React, { useState } from 'react';
import Child from './Child';
import Elements from './cy-element';
import Styles from './cy-style';

function App() {
  const [el, setElements] = useState(Elements);
  const [sy, setStyles] = useState(Styles);
  const onClick = (type: string) => {
    const node = el.find(e => e.data.id === "n01");
    node!.data.type = type;
    setElements([...el]);
  };
  return (
    <div>
      <Child
        Elements={el}
        Styles={sy}
      />
      <button onClick={() => onClick('')}>blank</button>
      <button onClick={() => onClick('A')}>A</button>
      <button onClick={() => onClick('B')}>B</button>
    </div>
  );
}

export default App;
import { ElementDefinition } from 'cytoscape';

export default [{
  "data": {
    "id": "n01",
    "label": "Node1",
    "type": "A"
  }
}, {
  "data": {
    "id": "n02",
    "label": "Node2"
  }
}, {
  "data": {
    "source": "n01",
    "target": "n02"
  }
}] as ElementDefinition[];
import { Stylesheet } from 'cytoscape';

export default
[
  {
    "selector": "node",
    "style": {
      'label': 'data(label)',
      "background-color": "black"
    }
  },
  {
    "selector": "node[type='A']",
    "style": {
      "background-color": "red"
    }
  },
  {
    "selector": "node[type='B']",
    "style": {
      "background-color": "blue"
    }
  }
] as Stylesheet[];
プロを目指す人のためのTypeScript入門 (Amazon)

コメント