React 126 ⚛️ Canvas Rendering with React Konva
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
| Component | Purpose |
|---|---|
<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
| Prop | Trigger |
|---|---|
onClick | Click on shape |
onMouseEnter / onMouseLeave | Pointer enter/leave |
onDragStart / onDragMove / onDragEnd | Drag lifecycle |
onTransformEnd | Transformer resize/rotate complete |
Performance Flags
| Prop | Effect |
|---|---|
listening={false} | Exclude from hit-testing |
cache() | Render to internal buffer |
clearCache() | Invalidate the buffer |
Separate <Layer> | Isolate redraw scope |
Version and Platform
| Constraint | Value |
|---|---|
| React 19 | react-konva current |
| React 18 | react-konva@18 |
| React Native | Not 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Dragged shape snaps back | Strict mode resets props on re-render | Use non-strict mode or sync position to state |
| Low FPS with many shapes | All shapes participate in hit-testing | listening={false} on non-interactive shapes |
| Drag lags | onDragMove triggers state updates | Update state only in onDragEnd |
| Version mismatch | react-konva major ≠ React major | Install react-konva@18 for React 18 |
| Cache not invalidated | Shape changes but cache remains | Call clearCache() on prop change |
| React Native error | No canvas element | Use 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
| Item | Value |
|---|---|
| Package | react-konva + konva |
| Hierarchy | Stage → Layer → Shapes |
| Shape components | Rect, Circle, Line, Text, Image |
| Event props | onClick, onDragEnd, onMouseEnter |
| Drag primitive | draggable + onDragEnd |
| Ref access | ref → Konva node instance |
| Performance flags | listening={false}, cache(), layer isolation |
| Version constraint | react-konva major = React major |
| React Native | Not 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
draggableprop is a complete drag-and-drop implementation. TheonDragEndhandler is where the final position synchronizes with React state.onDragMoveshould not callsetState. - The
refis 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.
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!