| |

React 126 ⚛️ Canvas Rendering with React Konva

⏱️ Estimated read: 14 min | |

react-konva is a React renderer for Konva, the 2D canvas scene graph library. The relationship is the same as React DOM and the browser DOM: you write components like <Rect> and <Circle>, the reconciler creates the corresponding Konva nodes, and Konva draws them onto a single <canvas> element . The library’s value is not that it makes canvas easy—canvas is never easy—but that it makes canvas declarative, with props, state, and event handlers that behave like their DOM counterparts.

This chapter covers react-konva as the 2D canvas layer for React applications. It begins with the Stage, Layer, and Shape hierarchy that mirrors Konva’s scene graph, then explains the event system, the drag-and-drop pattern, and the ref escape hatch that gives access to the underlying Konva node. The final sections address the performance features that matter at scale—listening={false}, caching, and layer isolation—and the version constraint that couples react-konva’s major version to React’s.


Key point: react-konva does not draw shapes. It creates Konva nodes, and Konva draws them. The React component tree is the scene graph, and the ref is the door to the imperative node when the declarative API is not enough.


Why react-konva exists

The declarative gap. Konva is an imperative scene graph. You create a Konva.Rect, set its x, y, width, height, and fill, add it to a Konva.Layer, and add the layer to a Konva.Stage. In a React application, that imperative management conflicts with the component model. react-konva resolves the conflict by letting the component tree describe the scene, and the reconciler creates and updates the Konva nodes .

The event system. A raw canvas has no built-in hit-testing. Konva provides it through its scene graph: every node knows its geometry, and the library can determine which shape is under a given point. react-konva exposes those events as React props—onClick, onMouseEnter, onDragEnd—so the developer writes the handler without implementing a raycast or a color-picking loop .

The drag-and-drop primitive. Konva’s draggable property is a complete drag-and-drop implementation. Setting draggable on a shape makes it draggable on desktop and mobile, and the onDragStart, onDragMove, and onDragEnd handlers report the position through e.target.x() and e.target.y(). This is the interaction that would require manual pointer tracking and hit-testing in a raw canvas, and it is the reason react-konva is the practical choice for design editors and whiteboards .

The version coupling. react-konva’s major version must match React’s major version. A React 19 project installs the current react-konva, while a React 18 project must install react-konva@18. The library does not work in React Native, because React Native has no DOM and no <canvas> element .


a. The Stage, Layer, and Shape hierarchy

Every react-konva application has the same three-level structure. The <Stage> is the root container; it wraps the <canvas> element and manages the renderer. The <Layer> is a separate canvas element that Konva uses for performance isolation; each layer has its own canvas, and changes to one layer do not force a redraw of the others. The shapes—<Rect>, <Circle>, <Line>, <Text>, <Image>—are the drawable nodes .

import { Stage, Layer, Rect, Circle, Text } from 'react-konva';

function App() {
  return (
    <Stage width={600} height={400}>
      <Layer>
        <Text text="Try clicking the shapes" x={20} y={20} fontSize={18} />
        <Rect
          x={50}
          y={80}
          width={120}
          height={80}
          fill="orange"
          onClick={() => console.log('rect clicked')}
        />
        <Circle
          x={300}
          y={120}
          radius={50}
          fill="skyblue"
          draggable
        />
      </Layer>
    </Stage>
  );
}

The props map directly to Konva node properties. x, y, width, height, fill, stroke, and opacity are the same names that Konva uses. The event props use React’s naming convention: onClick, onMouseEnter, onDragEnd, onTouchStart. The ref prop gives access to the underlying Konva node instance, which is the escape hatch to the imperative API .

import { useRef, useEffect } from 'react';
import { Circle } from 'react-konva';

function Shape() {
  const circleRef = useRef();

  useEffect(() => {
    // circleRef.current is a Konva.Circle instance.
    console.log(circleRef.current.radius());
  }, []);

  return <Circle ref={circleRef} radius={50} fill="black" />;
}

b. Events, drag-and-drop, and the strict mode distinction

The event system is the library’s primary interaction surface. A shape with an onClick handler receives the event when the user clicks within its geometry. The event object’s target is the Konva node that was clicked, and e.target.x() and e.target.y() return the node’s current position .

The drag-and-drop pattern is the most common interaction. The draggable prop enables dragging, and the onDragEnd handler is the place to synchronize the final position with React state. The onDragMove handler is for per-frame updates during the drag, and it should not call setState on every frame; the final position is what React needs to know .

import { useState } from 'react';
import { Stage, Layer, Circle } from 'react-konva';

function DraggableCircle() {
  const [position, setPosition] = useState({ x: 200, y: 200 });

  return (
    <Stage width={400} height={400}>
      <Layer>
        <Circle
          x={position.x}
          y={position.y}
          radius={50}
          fill="red"
          draggable
          onDragEnd={(e) => {
            // Synchronize the final position with React state.
            setPosition({ x: e.target.x(), y: e.target.y() });
          }}
        />
      </Layer>
    </Stage>
  );
}

The strict mode distinction is the library’s most subtle behavior. By default, react-konva works in non-strict mode: it updates only the properties that changed in render(). If the user drags a shape and the component re-renders for an unrelated reason, the shape keeps its dragged position. In strict mode, react-konva updates all properties to the values in render(), which means a dragged shape snaps back to its original position if the render does not include the new position. The mode is a per-component or global setting, and the choice depends on whether the component’s rendered props are the source of truth or a starting point .

// Global strict mode.
import { useStrictMode } from 'react-konva';
useStrictMode(true);

// Per-component strict mode.
<Rect width={50} height={50} fill="black" _useStrictMode />

c. Performance: listening, caching, and layer isolation

The performance of a react-konva scene is determined by the number of shapes that participate in event detection. Konva’s hit-testing walks the scene graph on every pointer event, and a scene with thousands of shapes that all have handlers will drop frames. The listening={false} prop removes a shape from event detection, which is the single most effective optimization for decorative or non-interactive shapes .

// Non-interactive shapes: remove them from hit-testing.
<Circle x={100} y={100} radius={10} fill="green" listening={false} />

Caching is the second optimization. A complex shape—one with many drawing operations, filters, or shadows—can be cached onto an internal canvas buffer. Once cached, Konva uses the buffer instead of redrawing the shape on every frame. The cache is invalidated with clearCache() when the shape changes. The documentation warns that caching simple shapes is counterproductive, and that caching a group of shapes is more efficient than caching each shape individually .

import { useRef, useEffect } from 'react';
import { Circle } from 'react-konva';

function CachedShape() {
  const ref = useRef();

  useEffect(() => {
    // Cache the shape after mount.
    ref.current.cache();
    return () => ref.current?.clearCache();
  }, []);

  return <Circle ref={ref} radius={50} fill="blue" shadowBlur={10} />;
}

Layer isolation is the structural optimization. Each <Layer> has its own canvas, and a change to one layer does not force a redraw of the others. A scene with a static background and an animated foreground should place them in separate layers, so the background is not redrawn on every frame . The stress test for 10,000 shapes uses a dedicated drag layer: the dragged shape is moved to that layer during the drag, and moved back on dragEnd, which keeps the drag smooth regardless of the main layer’s size .


Complete Example Session

// ============================================
// PART 1: THE BASIC STAGE
// ============================================
// Stage → Layer → Shapes.

import { Stage, Layer, Rect, Circle, Text } from 'react-konva';
import { useState, useRef, useEffect } from 'react';

function App() {
  const [color, setColor] = useState('orange');

  return (
    <Stage width={600} height={400}>
      <Layer>
        <Text text="Click the rect, drag the circle" x={20} y={20} />
        <Rect
          x={50} y={60} width={120} height={80}
          fill={color}
          onClick={() => setColor(color === 'orange' ? 'hotpink' : 'orange')}
        />
        <Circle x={300} y={100} radius={50} fill="skyblue" draggable />
      </Layer>
    </Stage>
  );
}

// ============================================
// PART 2: THE DRAG-AND-DROP STATE SYNC
// ============================================
// onDragEnd updates state. onDragMove does not.

function DraggableBox() {
  const [pos, setPos] = useState({ x: 200, y: 200 });
  return (
    <Rect
      x={pos.x} y={pos.y} width={100} height={60} fill="green"
      draggable
      onDragEnd={(e) => setPos({ x: e.target.x(), y: e.target.y() })}
    />
  );
}

// ============================================
// PART 3: THE REF ESCAPE HATCH
// ============================================
// Access the Konva node for imperative operations.

function ImperativeShape() {
  const circleRef = useRef();
  useEffect(() => {
    // Direct Konva API calls are possible through the ref.
    circleRef.current.shadowBlur(10);
    circleRef.current.getLayer().batchDraw();
  }, []);
  return <Circle ref={circleRef} x={100} y={100} radius={40} fill="purple" />;
}

// ============================================
// PART 4: THE PERFORMANCE: LISTENING FALSE
// ============================================
// Remove decorative shapes from hit-testing.

function Background({ count = 2000 }) {
  return Array.from({ length: count }).map((_, i) => (
    <Circle
      key={i}
      x={Math.random() * 800}
      y={Math.random() * 600}
      radius={2}
      fill="gray"
      listening={false}  // no event detection
    />
  ));
}

// ============================================
// PART 5: THE CACHING
// ============================================
// Cache complex shapes for repeated redraws.

function CachedGroup() {
  const groupRef = useRef();
  useEffect(() => {
    groupRef.current.cache();
    return () => groupRef.current?.clearCache();
  }, []);
  return (
    <Layer>
      <Circle ref={groupRef} x={400} y={300} radius={80} fill="red" shadowBlur={20} />
    </Layer>
  );
}

// ============================================
// PART 6: THE VERSION CONSTRAINT
// ============================================
// react-konva major must match React major.

// React 19: npm install react-konva konva
// React 18: npm install react-konva@18 konva

// ============================================
// PART 7: THE STRICT MODE
// ============================================
// Strict mode resets manually-changed props on re-render.

// Default: non-strict, dragged positions persist.
// Strict: all props reset to render values.

// ============================================
// PART 8: THE LAYER ISOLATION
// ============================================
// Separate layers for independent redraw.

function LayeredScene() {
  return (
    <Stage width={800} height={600}>
      <Layer listening={false}>
        <Background />
      </Layer>
      <Layer>
        <DraggableBox />
        <ImperativeShape />
      </Layer>
    </Stage>
  );
}

// ============================================
// PART 9: THE FULL PICTURE
// ============================================
// Declarative tree, imperative ref, performance flags.

export default function Root() {
  return <LayeredScene />;
}

The nine parts build a complete react-konva scene: the Stage and Layer hierarchy, the drag-and-drop state sync, the ref escape hatch, the listening={false} optimization, the caching pattern, and the version constraint. The pattern is consistent: the component tree describes the scene, the events synchronize with state, and the ref provides access to the imperative node when needed.


Quick Reference

Core Components

ComponentPurpose
<Stage>Root canvas container
<Layer>Independent canvas for performance isolation
<Rect>Rectangle shape
<Circle>Circle shape
<Text>Text rendering
<Image>Image rendering
<Transformer>Selection, resize, rotate handles

Event Props

PropTrigger
onClickClick on shape
onMouseEnter / onMouseLeavePointer enter/leave
onDragStart / onDragMove / onDragEndDrag lifecycle
onTransformEndTransformer resize/rotate complete

Performance Flags

PropEffect
listening={false}Exclude from hit-testing
cache()Render to internal buffer
clearCache()Invalidate the buffer
Separate <Layer>Isolate redraw scope

Version and Platform

ConstraintValue
React 19react-konva current
React 18react-konva@18
React NativeNot supported
DOM target<canvas> element

Best Practices

✅ Do This:

// Set listening={false} on decorative shapes.
<Circle listening={false} fill="gray" />                             // ✅
// Synchronize drag position in onDragEnd.
onDragEnd={(e) => setPos({ x: e.target.x(), y: e.target.y() })}      // ✅
// Use ref for imperative Konva API access.
const ref = useRef(); ref.current.cache();                           // ✅
// Match react-konva major to React major.
// React 19 → react-konva; React 18 → react-konva@18                // ✅
// Use separate layers for static and animated content.
<Layer><Background /></Layer><Layer><Animated /></Layer>             // ✅

❌ Don’t Do This:

// Don't setState in onDragMove.
onDragMove={(e) => setPos({ x: e.target.x(), y: e.target.y() })}     // ❌
// Don't cache simple shapes without filters.
ref.current.cache(); // on a plain Rect                               // ❌
// Don't use react-konva in React Native.
// No DOM, no canvas.                                                // ❌
// Don't mismatch versions.
// React 18 with react-konva 19 → incompatible.                      // ❌

Common Pitfalls

PitfallWhy It HappensFix
Dragged shape snaps backStrict mode resets props on re-renderUse non-strict mode or sync position to state
Low FPS with many shapesAll shapes participate in hit-testinglistening={false} on non-interactive shapes
Drag lagsonDragMove triggers state updatesUpdate state only in onDragEnd
Version mismatchreact-konva major ≠ React majorInstall react-konva@18 for React 18
Cache not invalidatedShape changes but cache remainsCall clearCache() on prop change
React Native errorNo canvas elementUse React Native Skia instead

Real-World Examples

1. Basic Shape

<Rect x={50} y={50} width={100} height={80} fill="orange" />

2. Draggable Circle

<Circle x={200} y={200} radius={50} fill="red" draggable />

3. Drag End Sync

onDragEnd={(e) => setPos({ x: e.target.x(), y: e.target.y() })}

4. Click Handler

<Rect onClick={() => setColor('blue')} />

5. Ref Access

const ref = useRef(); ref.current.cache();

6. Non-Listening Background

<Circle listening={false} fill="gray" />

7. Layer Isolation

<Layer><Static /></Layer><Layer><Animated /></Layer>

8. Strict Mode

useStrictMode(true);

9. Version Match

npm install react-konva@18 konva  # React 18

10. Transformer

<Transformer ref={trRef} />

Visual

The Scene Graph

┌─────────────────────────────────────────────────────────┐
│  <Stage>                                                │
│  → <canvas> element                                     │
│         │                                               │
│         ▼                                               │
│  <Layer>                                                │
│  → internal canvas buffer                               │
│         │                                               │
│    ┌────┴────┬──────────┐                               │
│    ▼         ▼          ▼                               │
│  <Rect>   <Circle>   <Text>                             │
│  → Konva.Rect  → Konva.Circle  → Konva.Text             │
│                                                         │
│  Each React component maps to a Konva node.             │
│  Konva draws the nodes onto the canvas.                 │
└─────────────────────────────────────────────────────────┘

The Drag Lifecycle

┌─────────────────────────────────────────────────────────┐
│  draggable={true}                                       │
│         │                                               │
│         ▼  pointer down                                 │
│  onDragStart → e.target is the shape                    │
│         │                                               │
│         ▼  pointer move                                 │
│  onDragMove → shape position updates (no setState)      │
│         │                                               │
│         ▼  pointer up                                   │
│  onDragEnd → setState with final position               │
│                                                         │
│  The shape moves without React re-rendering.            │
│  State syncs once, at the end.                          │
└─────────────────────────────────────────────────────────┘

The Performance Hierarchy

┌─────────────────────────────────────────────────────────┐
│  MOST IMPACTFUL                                         │
│  listening={false} on decorative shapes                 │
│  → removes them from hit-testing                        │
│                                                         │
│  Layer isolation                                        │
│  → static content not redrawn with animation            │
│                                                         │
│  Caching complex shapes                                 │
│  → internal buffer instead of redraw                    │
│                                                         │
│  LEAST IMPACTFUL (and risky)                            │
│  Caching simple shapes                                  │
│  → memory overhead without benefit                      │
└─────────────────────────────────────────────────────────┘

The Version Coupling

┌─────────────────────────────────────────────────────────┐
│  React 19 project                                       │
│  npm install react-konva konva                          │
│  → current version                                      │
│                                                         │
│  React 18 project                                       │
│  npm install react-konva@18 konva                       │
│  → v18 line                                             │
│                                                         │
│  Mismatch → runtime errors or silent failures           │
│  The major versions must align.                         │
└─────────────────────────────────────────────────────────┘

Summary

ItemValue
Packagereact-konva + konva
HierarchyStage → Layer → Shapes
Shape componentsRect, Circle, Line, Text, Image
Event propsonClick, onDragEnd, onMouseEnter
Drag primitivedraggable + onDragEnd
Ref accessref → Konva node instance
Performance flagslistening={false}, cache(), layer isolation
Version constraintreact-konva major = React major
React NativeNot supported

Key takeaways:

  • react-konva is a renderer, not a wrapper. The component tree is the Konva scene graph, and the reconciler creates the underlying nodes. The relationship is the same as React DOM and the browser DOM .
  • The Stage, Layer, and Shape hierarchy is the structure. The Stage wraps the canvas, the Layer provides a separate canvas for performance isolation, and the shapes are the drawable nodes .
  • The draggable prop is a complete drag-and-drop implementation. The onDragEnd handler is where the final position synchronizes with React state. onDragMove should not call setState .
  • The ref is the escape hatch. When the declarative API cannot express an operation, the ref gives access to the Konva node instance, and the full imperative API is available .
  • listening={false} is the most effective performance optimization. Removing decorative or non-interactive shapes from hit-testing eliminates the per-pointer-event scene graph walk .
  • Caching is for complex shapes, not simple ones. A shape with filters, shadows, or many drawing operations benefits from an internal buffer. A plain rectangle does not .
  • The version constraint is strict. react-konva’s major version must match React’s major version. A React 18 project uses react-konva@18; a React 19 project uses the current release .
  • react-konva does not work in React Native. React Native has no DOM and no <canvas> element. The alternative is React Native Skia .

Remember: react-konva is React for the 2D canvas. The component tree describes the scene, the event props handle interaction, and the ref provides access to the imperative node when the declarative API is not enough. The performance discipline is the discipline of any canvas application: remove non-interactive shapes from hit-testing, isolate layers, and cache only when the shape is expensive to redraw. The version coupling is the practical constraint, and the absence of React Native support is the platform limit. Build the scene declaratively, synchronize state at the end of interactions rather than during them, and let Konva handle the drawing.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!