3行で頼む
onNodeDrag
を呼び出す時に、ドラッグしているnode
のプロパティをreact-flowが直接編集する- ドラッグ前に何らかの形でその
node
をimmutableにすると、変更できずエラーになる - 本事象では
onNodeMouseEnter
イベント発生時に、node
をreduxのstateに含めてしまっていた
再現レポジトリ
https://github.com/takaneko/fata-20210908-reproduce-app
GitHubが動画のアップロードに対応していたと知り、再現動画も置いてみた。
※ 再現アプリではonNodeDoubleClick
イベントをバグ発生の契機にしている。
何が起きたか
react-flowを使ったReactアプリでonNodeDrag
イベントハンドラを登録すると、ノードをドラッグした時に
TypeError: Cannot assign to read only property 'x' of object '#<Object>'
エラーが発生してドラッグできない状態になってしまった。
エラー内容と発生箇所
TypeError: Cannot assign to read only property 'x' of object '#<Object>'
はそのまま「プロパティx
を変更できない」というエラーで、
react-flow/src/components/Nodes/wrapNode.tsxの以下の部分で発生している。
node.position.x += draggableData.deltaX; // ココでエラーになる
node.position.y += draggableData.deltaY;
onNodeDrag(event as MouseEvent, node);
node
が何らかの理由で変更できない状態になってしまったらしい。
node
はいつ変更できない状態になったのか
react-flowのコードを調べる
このnode
をreact-flowで定義している場所を見るとuseMemo
してるだけだったので、readonlyエラーが起こるようには見えない。
const node = useMemo(
() => ({ id, type, position: { x: xPos, y: yPos }, data }),
[id, type, xPos, yPos, data]
);
※ このnode
の値を直接編集するのってお行儀悪い気がするんだけど、バグとは関係ないのでとりあえず忘れた
自分の書いたコードを調べる
手当たり次第にコメントアウトして再現テストを繰り返したところ、onNodeMouseEnter
イベントハンドラをコメントアウトした時にエラーが発生しないことがわかった。
onNodeMouseEnter
イベントハンドラの実装
const onNodeMouseEnter = (
event: React.MouseEvent,
node: Node<any>
) => {
dispatch(mouseenter(node));
...
}
mouseenter
アクションの実装
export const flowSlice = createSlice({
...
mouseenter: (state: FlowState, action: PayloadAction<Node<any>>) => {
state.hoveredNode = action.payload;
},
...
};
mouseenter
イベントの引数node
をreduxのstateとして登録している。
このnode
はエラー発生箇所で変更しようとしているnode
とイコールで、mouseenter
イベントはノードをドラッグする前に絶対に発生しているので、このイベントでimmutableにしているのが原因だとわかった。
バグの修正
elements
(react-flowに登録するノード全ての配列)からnode
と同じidの要素を取り出して、それをreduxのstate管理するように変更した。
const onNodeMouseEnter = (
event: React.MouseEvent,
node: Node<any>
) => {
const elem = elements.find((e) => e.id === node.id);
if (!elem) return;
dispatch(mouseenter(elem));
...
}
取り出したelement
はonNodeMouseEnter
イベントハンドラのnode
と同じ内容だが、react-flow側で管理しているnode
オブジェクトとは参照が異なるため、onNodeDrag
呼び出し時にreact-flow側でnode.position
を変更してもエラーにならない。
まとめ
ライブラリ側のイベントハンドラの引数をそのままstate管理したりすると思わぬバグを踏むことがわかった。
react-flow側のコードが ↓ のようになっていてもいい気がするので、Issue or MR作って聞いてみたい。
const draggingNode = {
...node,
position: {
x: node.position.x + draggableData.deltaX,
y: node.position.y + draggableData.deltaY,
},
};
onNodeDrag(event as MouseEvent, draggingNode);
(パフォーマンス的に今のコードの方がいいのかな?)