TL;DR
絶対に再レンダリングさせたくない
jQueryなどで直接DOM操作しているライブラリをReactから呼び出して使うと、すこし困ったことがあります。Reactはコンポーネントが管理しているStateに変化があると、コンポーネントの持つHTML(JSX)を再レンダリングするのですが、jQueryなどでレンダリングした部分が消えてしまうためもう一度jQueryでレンダリングを行う必要があります。
この際に、jQueryライブラリが内部で持っている状態(State)が消えてしまい、表示が初期状態に戻ってしまうことがあります。 renderを実装しているjQueryライブラリであれば、この問題を回避することが出来ますが、多くのライブラリではインスタンスを作成するときにしかレンダリングを提供していないことが多いと思います。
この場合出来るだけReact外のレンダリングを持つインスタンスの初期化を避けた方が良いです。
useMemoを使ってレンダリングされないElementを取得する
useMemoは、値をメモ化し不要な再計算を行わないように値を返してくれます。この中でjQueryライブラリ等を実行するだけのコンポーネントを呼び出します。( ※ コンポーネントの引数が無い場合でも同様の効果が得られますが、初回レンダリングで値を渡しレンダリングする必要があるため、useMemoを使った方が管理しやすいです)
const graph = useMemo( () => <Graph list={list} />, [] );
useMemoは第二引数に [] を指定すると初回のみレンダリングされます。list変数は初回の値のみメモ化されるため、listの値を変更してもGraphには新しいlistの中身が伝わりません。
これで再レンダリングを防ぐことが出来たので、あとはlistの更新方法を考えるだけです。もちろん useMemoの第二引数にlistを入れる方法は、listが更新されるたびにGraphが再レンダリングされるため使えません。
この場合Graph内でaddEventListener() でイベントの監視をして、親要素から値の変更を検知したらイベントを送るように実装します。
そのためは、まずjQueryなどで定義されたElementのrefを取得します。
refの転送を行う React.fowardRef
React.fowardRefは、コンポーネントにrefを指定すると、そのコンポーネントのrefではなく子コンポーネントのrefが参照されるようになります。
const Graph = React.forwardRef((props, ref) => ( <div ref={ref} className="graph"> {props.children} </div> ));
ref.currentの中身にはレンダリング済みのHTML DOMが入っており、このDOMに対して dispatchEvent() することで直接jQueryライブラリがレンダリングしたDOMに対してイベントを発火させることができます。
値を持たせたイベントを発火する
Eventを発火させるには、Eventを継承してカスタマイズしたものを利用しますが、もっと楽にカスタムイベントを作成できる機能が用意されています。
const event = new CustomEvent('updateList', { detail: list });
CustomEvent は 引数に { detail: any; } を持ち、任意のパラメータを渡せます。これによってlistを渡し、レンダリングを避けたい子コンポーネント側でイベントを購読し、ライブラリ内部による状態変更を行い、DOMを変更し、再レンダリングさせることなく状態変更出来ます。
補足
- refのイベントを監視しなくても初めからライブラリが用意しているイベントを発火しても良いです。
- Nextjsなのでホットリロードすると、二重でイベントを監視してしまうことがあるため、
.removeEventListener
してから.addEventListener
します。