4 min read

Event Handling and State Management in React Canvas App

In the development of interactive canvas applications using React and Konva, managing events and state effectively is crucial for creating a seamless user experience. During my recent project, I encountered several challenges related to event handling and state management that taught me valuable lessons in solving these issues. In this blog post, I’ll delve into the core problem I faced, how I approached solving it, and the insights gained from the experience.

Central Challenge

The primary challenge I faced was ensuring that transformations and interactions with canvas nodes (such as dragging and resizing) were accurately reflected in the application state managed by Redux. This issue was compounded by event bubbling and improper state updates, which led to inconsistencies and bugs in the user interface.

Observations and Approach

1. Event Bubbling Interference

One of the key issues was that event bubbling was causing unintended interactions between the transformer handles and the underlying shapes. When transforming a node, clicks on transformer anchors were incorrectly being interpreted as clicks on shapes, leading to incorrect selections and updates.

Solution and Insights:

To address this, I implemented a mechanism to differentiate between clicks on transformer handles and the shapes themselves. By checking the name of the event target for _anchor, I could effectively filter out clicks on anchors and prevent them from triggering shape selection logic.

Code Snippet:

const handleStageMouseDown = (e: Konva.KonvaEventObject<Event>) => {
const clickedOnShape = e.target instanceof Konva.Shape;
if (e.target.name() && e.target.name().includes("_anchor")) {
console.log("Click on anchor, do nothing");
return;
}
if (clickedOnShape) {
console.log("Selecting shape", e.target);
// Existing selection logic...
}
};

This approach helped ensure that interactions with transformer handles did not interfere with the underlying shape selection.

2. State Management and Updates

Another significant challenge was updating the canvas node properties in Redux. The initial implementation replaced entire node objects, which led to loss of specific properties and incorrect state management.

Solution and Insights:

I refined the Redux update logic to handle partial updates more effectively. By separating top-level properties from nested props, and then merging updates accordingly, I could ensure that only the necessary parts of the state were updated without overwriting existing data.

Redux Slice Update:

const updateNode = (state: any, action: PayloadAction<Partial<CanvasNodeData> & { id: string }>) => {
console.log("Slice update node with ID:", action.payload.id, "with payload:", action.payload);
const index = state.nodes.findIndex(
(node: CanvasNodeData) => node.id === action.payload.id
);
if (index === -1) {
return;
}
const topLevelProps: (keyof CanvasNodeData)[] = ["id", "type", "label", "x", "y"];
const nodeToUpdate = state.nodes[index];
const newProps: { [key: string]: any } = { ...nodeToUpdate.props };
for (const key in action.payload) {
if (topLevelProps.includes(key as keyof CanvasNodeData)) {
nodeToUpdate[key as keyof CanvasNodeData] = action.payload[key as keyof CanvasNodeData];
} else {
newProps[key] = action.payload[key as keyof CanvasNodeData];
}
}
nodeToUpdate.props = newProps;
};

This approach ensured that updates were applied correctly without losing data or causing state inconsistencies.

3. Component Organization

Lastly, organizing the NodeComponent to handle rendering and event management was essential for maintaining clean and manageable code. By extracting NodeComponent and properly passing event handlers, I improved the interaction between Konva nodes and React components.

Refactored NodeComponent:

const NodeComponent = (props: NodeComponentProps) => {
const nodeInstance = utils.createCanvasNodeInstance(props.data);
const config = nodeInstance.getNodeConfig();
config.onDragStart = props.onDragStart;
config.onDragEnd = props.onDragEnd;
config.onDragMove = props.onDragMove;
switch (props.data.type) {
case "rect":
return <Rect {...config} />;
case "circle":
return <Circle {...config} />;
case "group":
return <Group {...config} />;
default:
return null;
}
};

Usage in Rendering:

{nodes.map((node) => (
<NodeComponent
key={node.id}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragMove={handleDragMove}
data={node}
/>
))}

This refactoring streamlined the rendering process and ensured that event handling was consistent across different types of nodes.

Conclusion

The journey of solving event handling and state management issues in a React and Konva canvas application has been both challenging and enlightening. By addressing event bubbling, refining state updates, and organizing components effectively, I was able to create a more robust and maintainable application.

These experiences underscore the importance of careful event management and precise state handling in interactive applications. I hope this reflection provides valuable insights for others facing similar challenges in their development projects.