Use React Refs to Manipulate the DOM and Konva to Add Graphics
Tags: react, javascript, webdev, programming
Published: 2021-01-20 23:04:56

**Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62**

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

React is a flexible framework that provides structured UI code while allowing the flexibility to manipulate DOM elements directly. All React components can be accessed by their refs. A React `ref` provides access to the underlying HTML DOM element that you can manipulate directly.

To use refs, we use the `React.createRef` function the `useRef` hook to create a `ref` object and then assign it to a variable. he variable is set as the value of the `ref` prop.

For example, we define a ref with:

```
const inputRef = React.createRef();
```

Then in the `input` element, we add the following:

```
<input type="text" ref={inputRef} />
```

Then we can access the input element by adding:

```
inputRef.current
```

This provides access to the HTML element, and we can utilize native DOM functionality. In our app, we will use the `useRef` hook, and you will see how to implement that below.

What we are building
--------------------

In this article, we will build a whiteboard app that allows users to add shapes, texts, lines, and images to a whiteboard. In addition, users can undo their work and erase stuff from the screen.

We use the Konva library to let us add the shapes/text and erase them. The [Konva](https://konvajs.org/) library abstracts the hard work of adding items to the canvas. It allows you to add many shapes by simply writing a few lines of code. There are also React bindings for Konva, which abstracts some functionality even further for React. However, the feature set of React Konva is rather limited, so in order to meet the requirements of most apps, React Konva should be used as a companion to Konva.

We also want to allow users to move and transform your shapes easily, which you would have to write yourself if you wanted to do it in the [HTML Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API).

Konva works by creating a stage and a layer in the stage which will allow you to add the lines, shapes, and text that you want.

Getting started
---------------

To start, we will create a React app with the Create React App command line program. Run `npx create-react-app whiteboard-app` to create the initial files for our app. Next, we need to add some packages. We want to use Bootstrap for styling, in addition to the Konva packages, and helper package for creating unique IDs for our shapes, lines, and text. We also need React Router for routing.

To install the libraries, we run:

```
npm i bootstrap react-bootstrap konva react-konva react-router-dom use-image uuid
```

`use-image` is a package to convert image URLs into image objects that can be displayed on canvas. The UUID package generates unique IDs for our shapes.

With the packages installed, we can start writing code. First we start with the entry point of our app, which is `App.js`. Replace the existing code of the file with:

```
import React from "react";  
import { Router, Route, Link } from "react-router-dom";  
import HomePage from "./HomePage";  
import TopBar from "./TopBar";  
import { createBrowserHistory as createHistory } from "history";  
import "./App.css";  
const history = createHistory();
function App() {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <TopBar />  
        <Route path="/" exact component={HomePage} />  
      </Router>  
    </div>  
  );  
}

export default App;
```

All we added is a top navigation bar and our only route which is the home page.

Next we add the code for the shapes. React Konva has libraries for common shapes like rectangles and circles. We first start with a circle. In the `src`  
folder, create a file called `Circle.js` and add:

```
import React from "react";  
import { Circle, Transformer } from "react-konva";
const Circ = ({ shapeProps, isSelected, onSelect, onChange }) => {  
  const shapeRef = React.useRef();  
  const trRef = React.useRef();
  React.useEffect(() => {  
    if (isSelected) {  
      trRef.current.setNode(shapeRef.current);  
      trRef.current.getLayer().batchDraw();  
    }  
  }, [isSelected]);

  return (  
    <React.Fragment>  
      <Circle  
        onClick={onSelect}  
        ref={shapeRef}  
        {...shapeProps}  
        draggable  
        onDragEnd={e => {  
          onChange({  
            ...shapeProps,  
            x: e.target.x(),  
            y: e.target.y(),  
          });  
        }}  
        onTransformEnd={e => {  
          // transformer is changing scale  
          const node = shapeRef.current;  
          const scaleX = node.scaleX();  
          const scaleY = node.scaleY();  
          node.scaleX(1);  
          node.scaleY(1);  
          onChange({  
            ...shapeProps,  
            x: node.x(),  
            y: node.y(),  
            width: node.width() * scaleX,  
            height: node.height() * scaleY,  
          });  
        }}  
      />  
      {isSelected && <Transformer ref={trRef} />}  
    </React.Fragment>  
  );  
};

export default Circ;
```

This code returns the Circle shape which can be added onto the canvas at will. In the `React.useEffect`’s callback function, we detect if the shape is selected and then draw a handle for the shape so that it can be resized and moved.

In this file, we added refs to the `Circle` component so that we can access it in the `useEffect` callback. The `setNode` function takes an HTML DOM element.

The component in the `return` statement is the main code for the `Circle` . We have an `onClick` handler that gets the ID of the selected shape. The `draggable` prop makes the Circle draggable. `onDragEnd` handles the event when the user stops dragging. The position is updated there. `onTransformEnd` scales the shape as the user drags the handles that are available. The `width` and `height` are changed as the handles are dragged. `{isSelected && <Transformer ref={trRef} />}` create the `Transformer` object, which is a Konva object that lets you change the size of a shape when you select.

Next we add a component for the image. Create a file called `Image.js` in the `src` folder and add the following:

```
import React from "react";  
import { Image, Transformer } from "react-konva";  
import useImage from "use-image";
const Img = ({ shapeProps, isSelected, onSelect, onChange, imageUrl }) => {  
  const shapeRef = React.useRef();  
  const trRef = React.useRef();  
  const [image] = useImage(imageUrl); 

  React.useEffect(() => {  
    if (isSelected) {  
      trRef.current.setNode(shapeRef.current);  
      trRef.current.getLayer().batchDraw();  
    }  
  }, [isSelected]); 
  return (  
    <React.Fragment>  
      <Image  
        onClick={onSelect}  
        image={image}  
        ref={shapeRef}  
        draggable  
        onDragEnd={e => {  
          onChange({  
            ...shapeProps,  
            x: e.target.x(),  
            y: e.target.y(),  
          });  
        }}  
        onTransformEnd={e => {  
          const node = shapeRef.current;  
          const scaleX = node.scaleX();  
          const scaleY = node.scaleY();  
          onChange({  
            ...shapeProps,  
            x: node.x(),  
            y: node.y(),  
            width: node.width() * scaleX,  
            height: node.height() * scaleY,  
          });  
        }}  
      />  
      {isSelected && <Transformer ref={trRef} />}  
    </React.Fragment>  
  );  
};

export default Img;
```

This is very similar to the `Circle` component except we have the `useImage` function provided by the `use-image` library to convert the given `imageUrl` prop to an image that is displayed on the canvas.

In this file, we added refs to the `Image` component so that we can access it in the `useEffect` callback. The `setNode` function takes an HTML DOM element.

Next we create a free drawing line. Create a file called `line.js` in the `src` folder and add:

```
import Konva from "konva";

export const addLine = (stage, layer, mode = "brush") => {  
  let isPaint = false;  
  let lastLine; stage.on("mousedown touchstart", function(e) {  
    isPaint = true;  
    let pos = stage.getPointerPosition();  
    lastLine = new Konva.Line({  
      stroke: mode == "brush" ? "red" : "white",  
      strokeWidth: mode == "brush" ? 5 : 20,  
      globalCompositeOperation:  
        mode === "brush" ? "source-over" : "destination-out",  
      points: [pos.x, pos.y],  
      draggable: mode == "brush",  
    });  
    layer.add(lastLine);  
  }); 

  stage.on("mouseup touchend", function() {  
    isPaint = false;  
  }); 

  stage.on("mousemove touchmove", function() {  
    if (!isPaint) {  
      return;  
    } const pos = stage.getPointerPosition();  
    let newPoints = lastLine.points().concat(\[pos.x, pos.y\]);  
    lastLine.points(newPoints);  
    layer.batchDraw();  
  });  
};
```

In this file, we use plain Konva since React Konva does not have a convenient way to make free drawing a line where a user drags the mouse. When the `mousedown` and `touchstart` is triggered, we set the color of the line depending on what the `mode` is. When it is `brush`, we draw a red line. If it’s `erase` we draw a thick white line so that users can draw it over their content, letting users erase their changes.

When the `mousemove` and `touchend` events are triggered, we set `isPaint` to false so we stop drawing the line. When the `mousemove` and `touchmove` events are triggered, we add dots along the way to draw the line in the direction the user wants when the user moves the mouse when clicking or touching the touchscreen.

`stage` and `layer` are the Konva Stage and Layer objects which we pass in when the `addLine` function is called.

Next we create the Rectangle component for drawing free form rectangles. In the `src` folder, create a file called `Rectangle.js` and add:

```
import React from "react";  
import { Rect, Transformer } from "react-konva";
const Rectangle = ({ shapeProps, isSelected, onSelect, onChange }) => {  
  const shapeRef = React.useRef();  
  const trRef = React.useRef(); 
  React.useEffect(() => {  
    if (isSelected) {  
      // we need to attach transformer manually  
      trRef.current.setNode(shapeRef.current);  
      trRef.current.getLayer().batchDraw();  
    }  
  }, [isSelected]); 

  return (  
    <React.Fragment>  
      <Rect  
        onClick={onSelect}  
        ref={shapeRef}  
        {...shapeProps}  
        draggable  
        onDragEnd={e => {  
          onChange({  
            ...shapeProps,  
            x: e.target.x(),  
            y: e.target.y(),  
          });  
        }}  
        onTransformEnd={e => {  
          // transformer is changing scale  
          const node = shapeRef.current;  
          const scaleX = node.scaleX();  
          const scaleY = node.scaleY();  
          node.scaleX(1);  
          node.scaleY(1);  
          onChange({  
            ...shapeProps,  
            x: node.x(),  
            y: node.y(),  
            width: node.width() * scaleX,  
            height: node.height() * scaleY,  
          });  
        }}  
      />  
      {isSelected && <Transformer ref={trRef} />}  
    </React.Fragment>  
  );  
};export default Rectangle;
```

This component is similar to `Circle` component. We have the drag handles to move and resize the rectangle by adding the `onDragEnd` and `onTransformEnd` callbacks, change the `x` and `y` coordinates in the `onDragEnd` handler, and change the `width` and `height` in the `onTransformEnd` event callback.

The `Transformer` component is added if the shape is selected so that users can move or resize the shape with the handles when selected.

Similar to the `Circle` component, we added refs to the `Rectangle` component so that we can access it in the `useEffect` callback. The `setNode` function takes an HTML DOM element.

Next we add a `text` field component to let users can add text to the whiteboard. Create a file called `textNode.js` and add the following:

```
import Konva from "konva";  
const uuidv1 = require("uuid/v1");
export const addTextNode = (stage, layer) => {  
  const id = uuidv1();  
  const textNode = new Konva.Text({  
    text: "type here",  
    x: 50,  
    y: 80,  
    fontSize: 20,  
    draggable: true,  
    width: 200,  
    id,  
  }); 

  layer.add(textNode); let tr = new Konva.Transformer({  
    node: textNode,  
    enabledAnchors: ["middle-left", "middle-right"],  
    // set minimum width of text  
    boundBoxFunc: function(oldBox, newBox) {  
      newBox.width = Math.max(30, newBox.width);  
      return newBox;  
    },  
  }); 

  stage.on("click", function(e) {  
    if (!this.clickStartShape) {  
      return;  
    }  
    if (e.target._id == this.clickStartShape._id) {  
      layer.add(tr);  
      tr.attachTo(e.target);  
      layer.draw();  
    } else {  
      tr.detach();  
      layer.draw();  
    }  
  }); 

  textNode.on("transform", function() {  
    // reset scale, so only with is changing by transformer  
    textNode.setAttrs({  
      width: textNode.width() \* textNode.scaleX(),  
      scaleX: 1,  
    });  
  }); 

  layer.add(tr); layer.draw(); textNode.on("dblclick", () => {  
    // hide text node and transformer:  
    textNode.hide();  
    tr.hide();  
    layer.draw(); 
    // create textarea over canvas with absolute position  
    // first we need to find position for textarea  
    // how to find it?
    // at first lets find position of text node relative to the stage:  
    let textPosition = textNode.absolutePosition();
    // then lets find position of stage container on the page:  
    let stageBox = stage.container().getBoundingClientRect();
    // so position of textarea will be the sum of positions above:  
    let areaPosition = {  
      x: stageBox.left + textPosition.x,  
      y: stageBox.top + textPosition.y,  
    };
    // create textarea and style it  
    let textarea = document.createElement("textarea");  
    document.body.appendChild(textarea);
    // apply many styles to match text on canvas as close as possible  
    // remember that text rendering on canvas and on the textarea can be different  
    // and sometimes it is hard to make it 100% the same. But we will try...  
    textarea.value = textNode.text();  
    textarea.style.position = "absolute";  
    textarea.style.top = areaPosition.y + "px";  
    textarea.style.left = areaPosition.x + "px";  
    textarea.style.width = textNode.width() - textNode.padding() * 2 + "px";  
    textarea.style.height =  
      textNode.height() - textNode.padding() * 2 + 5 + "px";  
    textarea.style.fontSize = textNode.fontSize() + "px";  
    textarea.style.border = "none";  
    textarea.style.padding = "0px";  
    textarea.style.margin = "0px";  
    textarea.style.overflow = "hidden";  
    textarea.style.background = "none";  
    textarea.style.outline = "none";  
    textarea.style.resize = "none";  
    textarea.style.lineHeight = textNode.lineHeight();  
    textarea.style.fontFamily = textNode.fontFamily();  
    textarea.style.transformOrigin = "left top";  
    textarea.style.textAlign = textNode.align();  
    textarea.style.color = textNode.fill();  
    let rotation = textNode.rotation();  
    let transform = "";  
    if (rotation) {  
      transform += "rotateZ(" + rotation + "deg)";  
    } let px = 0;  
    let isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;  
    if (isFirefox) {  
      px += 2 + Math.round(textNode.fontSize() / 20);  
    }  
    transform += "translateY(-" + px + "px)"; textarea.style.transform = transform;  
    textarea.style.height = "auto";  
    // after browsers resized it we can set actual value  
    textarea.style.height = textarea.scrollHeight + 3 + "px"; textarea.focus(); 

    function removeTextarea() {  
      textarea.parentNode.removeChild(textarea);  
      window.removeEventListener("click", handleOutsideClick);  
      textNode.show();  
      tr.show();  
      tr.forceUpdate();  
      layer.draw();  
    } 

    function setTextareaWidth(newWidth) {  
      if (!newWidth) {  
        // set width for placeholder  
        newWidth = textNode.placeholder.length * textNode.fontSize();  
      }  
      // some extra fixes on different browsers  
      let isSafari = /^((?!chrome|android).)\*safari/i.test(navigator.userAgent);  
      let isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;  
      if (isSafari || isFirefox) {  
        newWidth = Math.ceil(newWidth);  
      } let isEdge = document.documentMode || /Edge/.test(navigator.userAgent);  
      if (isEdge) {  
        newWidth += 1;  
      }  
      textarea.style.width = newWidth + "px";  
    } 

    textarea.addEventListener("keydown", function(e) {  
      // hide on enter  
      // but don't hide on shift + enter  
      if (e.keyCode === 13 && !e.shiftKey) {  
        textNode.text(textarea.value);  
        removeTextarea();  
      }  
      // on esc do not set value back to node  
      if (e.keyCode === 27) {  
        removeTextarea();  
      }  
    }); 

    textarea.addEventListener("keydown", function(e) {  
      let scale = textNode.getAbsoluteScale().x;  
      setTextareaWidth(textNode.width() * scale);  
      textarea.style.height = "auto";  
      textarea.style.height =  
        textarea.scrollHeight + textNode.fontSize() + "px";  
    }); 

    function handleOutsideClick(e) {  
      if (e.target !== textarea) {  
        removeTextarea();  
      }  
    }  
    setTimeout(() => {  
      window.addEventListener("click", handleOutsideClick);  
    });  
  });  
  return id;  
};
```

We add a text area, and then we handle the events created by the text area. When the user clicks the text area, a box will with handles will be displayed to let the user move the text area around the canvas. This is what the `click` handler for the stage is doing. It finds the text area by ID and then attaches a Konva`Transformer` to it, adding the box with handles.

We have a `transform` handler for the `textNode` text area to resize the text area when the user drags the handles. We have a double click handler to let users enter text when they double click. Most of the code is for styling the text box as close to the canvas as possible so that it will blend into the canvas. Otherwise, it will look strange. We also let users rotate the text area by applying CSS for rotating the text area as the user drags the handles.

In the `keydown` event handler, we change the size of the text area as the user types to make sure it displays all the text without scrolling.

When the user clicks outside the text area, the box with handles will disappear, letting the user select other items.

The home page is where we put everything together. Create a new file called `HomePage.js` in the `src` folder and add:

```
import React, { useState, useRef } from "react";  
import ButtonGroup from "react-bootstrap/ButtonGroup";  
import Button from "react-bootstrap/Button";  
import "./HomePage.css";  
import { Stage, Layer } from "react-konva";  
import Rectangle from "./Rectangle";  
import Circle from "./Circle";  
import { addLine } from "./line";  
import { addTextNode } from "./textNode";  
import Image from "./Image";  
const uuidv1 = require("uuid/v1");

function HomePage() {  
  const [rectangles, setRectangles] = useState([]);  
  const [circles, setCircles] = useState([]);  
  const [images, setImages] = useState([]);  
  const [selectedId, selectShape] = useState(null);  
  const [shapes, setShapes] = useState([]);  
  const [, updateState] = React.useState();  
  const stageEl = React.createRef();  
  const layerEl = React.createRef();  
  const fileUploadEl = React.createRef(); 
  const getRandomInt = max => {  
    return Math.floor(Math.random() * Math.floor(max));  
  }; 
  const addRectangle = () => {  
    const rect = {  
      x: getRandomInt(100),  
      y: getRandomInt(100),  
      width: 100,  
      height: 100,  
      fill: "red",  
      id: `rect${rectangles.length + 1}`,  
    };  
    const rects = rectangles.concat(\[rect\]);  
    setRectangles(rects);  
    const shs = shapes.concat([`rect${rectangles.length + 1}`]);  
    setShapes(shs);  
  }; 

  const addCircle = () => {  
    const circ = {  
      x: getRandomInt(100),  
      y: getRandomInt(100),  
      width: 100,  
      height: 100,  
      fill: "red",  
      id: `circ${circles.length + 1}`,  
    };  
    const circs = circles.concat([circ]);  
    setCircles(circs);  
    const shs = shapes.concat([`circ${circles.length + 1}`]);  
    setShapes(shs);  
  }; 

  const drawLine = () => {  
    addLine(stageEl.current.getStage(), layerEl.current);  
  }; 

  const eraseLine = () => {  
    addLine(stageEl.current.getStage(), layerEl.current, "erase");  
  }; 

  const drawText = () => {  
    const id = addTextNode(stageEl.current.getStage(), layerEl.current);  
    const shs = shapes.concat([id]);  
    setShapes(shs);  
  }; 
  
  const drawImage = () => {  
    fileUploadEl.current.click();  
  }; 

  const forceUpdate = React.useCallback(() => updateState({}), []); 
  const fileChange = ev => {  
    let file = ev.target.files[0];  
    let reader = new FileReader(); 
    reader.addEventListener(  
      "load",  
      () => {  
        const id = uuidv1();  
        images.push({  
          content: reader.result,  
          id,  
        });  
        setImages(images);  
        fileUploadEl.current.value = null;  
        shapes.push(id);  
        setShapes(shapes);  
        forceUpdate();  
      },  
      false  
    );
    if (file) {  
      reader.readAsDataURL(file);  
    }  
  }; 

  const undo = () => {  
    const lastId = shapes[shapes.length - 1];  
    let index = circles.findIndex(c => c.id == lastId);  
    if (index != -1) {  
      circles.splice(index, 1);  
      setCircles(circles);  
    } 
    index = rectangles.findIndex(r => r.id == lastId);  
    if (index != -1) {  
      rectangles.splice(index, 1);  
      setRectangles(rectangles);  
    } 
    index = images.findIndex(r => r.id == lastId);  
    if (index != -1) {  
      images.splice(index, 1);  
      setImages(images);  
    }  
    shapes.pop();  
    setShapes(shapes);  
    forceUpdate();  
  }; 

  document.addEventListener("keydown", ev => {  
    if (ev.code == "Delete") {  
      let index = circles.findIndex(c => c.id == selectedId);  
      if (index != -1) {  
        circles.splice(index, 1);  
        setCircles(circles);  
      } 
      index = rectangles.findIndex(r => r.id == selectedId);  
      if (index != -1) {  
        rectangles.splice(index, 1);  
        setRectangles(rectangles);  
      } 
      index = images.findIndex(r => r.id == selectedId);  
      if (index != -1) {  
        images.splice(index, 1);  
        setImages(images);  
      }  
      forceUpdate();  
    }  
  }); 

  return (  
    <div className="home-page">  
      <h1>Whiteboard</h1>  
      <ButtonGroup>  
        <Button variant="secondary" onClick={addRectangle}>  
          Rectangle  
        </Button>  
        <Button variant="secondary" onClick={addCircle}>  
          Circle  
        </Button>  
        <Button variant="secondary" onClick={drawLine}>  
          Line  
        </Button>  
        <Button variant="secondary" onClick={eraseLine}>  
          Erase  
        </Button>  
        <Button variant="secondary" onClick={drawText}>  
          Text  
        </Button>  
        <Button variant="secondary" onClick={drawImage}>  
          Image  
        </Button>  
        <Button variant="secondary" onClick={undo}>  
          Undo  
        </Button>  
      </ButtonGroup>  
      <input  
        style={{ display: "none" }}  
        type="file"  
        ref={fileUploadEl}  
        onChange={fileChange}  
      />  
      <Stage  
        width={window.innerWidth * 0.9}  
        height={window.innerHeight - 150}  
        ref={stageEl}  
        onMouseDown={e => {  
          // deselect when clicked on empty area  
          const clickedOnEmpty = e.target === e.target.getStage();  
          if (clickedOnEmpty) {  
            selectShape(null);  
          }  
        }}  
      >  
        <Layer ref={layerEl}>  
          {rectangles.map((rect, i) => {  
            return (  
              <Rectangle  
                key={i}  
                shapeProps={rect}  
                isSelected={rect.id === selectedId}  
                onSelect={() => {  
                  selectShape(rect.id);  
                }}  
                onChange={newAttrs => {  
                  const rects = rectangles.slice();  
                  rects[i] = newAttrs;  
                  setRectangles(rects);  
                }}  
              />  
            );  
          })}  
          {circles.map((circle, i) => {  
            return (  
              <Circle  
                key={i}  
                shapeProps={circle}  
                isSelected={circle.id === selectedId}  
                onSelect={() => {  
                  selectShape(circle.id);  
                }}  
                onChange={newAttrs => {  
                  const circs = circles.slice();  
                  circs\[i\] = newAttrs;  
                  setCircles(circs);  
                }}  
              />  
            );  
          })}  
          {images.map((image, i) => {  
            return (  
              <Image  
                key={i}  
                imageUrl={image.content}  
                isSelected={image.id === selectedId}  
                onSelect={() => {  
                  selectShape(image.id);  
                }}  
                onChange={newAttrs => {  
                  const imgs = images.slice();  
                  imgs[i] = newAttrs;  
                }}  
              />  
            );  
          })}  
        </Layer>  
      </Stage>  
    </div>  
  );  
}

export default HomePage;
```

This is where we add the buttons to create the shapes. For the shapes provided by React Konva, we create the shapes by adding an object to the array for the shape and then map them to the shape with the properties specified by the object.

For example, to add a rectangle, we create an object, add it to the array by pushing the object and then calling `setRectangles` and then map them to the actual `Rectangle` component when we render the canvas. We pass in the `onSelect` handler so that the user can click on the shape and get the ID of the selected shape. The `onChange` handler lets us update the properties of an existing shape and then update the corresponding array for the shapes.

Every React Konva shape we add should be inside the `Layer` component. They provide the place for the shapes to reside. The `Stage` component provides a place for the `Layer` to be in. We set the `ref` prop for the `Stage` and `Layer` components so that we can access it directly.

In the call to the `addLine` function, we get the refs for the `Stage` and `Layer` components to get the reference to the Konva Stage and Layer instances so that we can use them in the `addLine` function. Note that to get the Konva Stage object, we have to call `setStage` after the the `current` attribute.

In the `Stage` component, we have a `onMouseDown` handler to deselect all shapes when the click is outside all the shapes.

To the undo a change, we keep track of all the shapes with the `shapes` array and then when the Undo button is clicked, then the last shape is removed from the array and also the corresponding shapes array. For example, if the undo removes a rectangle, then it will be removed from the `shapes` array and the `rectangle` array. The `shapes` array is an array of IDs of all the shapes.

To build the image upload feature, we add an input element which we don’t show, and we use the `ref` to write a function to let the user click on the hidden file input. Once the file input is clicked the user can choose files and the image is read with the `FileReader` object into base64, which will be converted to an image displayed on the canvas with the `use-image` library.

Similarly for letting users delete shapes when a shape is selected with the delete key, we add a key down handler. In the key down handler function, when a delete key event is triggered then the handler will find the shapes in the arrays by ID and delete it. It will also delete it from the `shapes` array. We defined the `forceUpdate` function so that the canvas will be updated even when there is DOM manipulation done without going through React. The `keydown` handler is added by using `document.addEventListener` which is not React code, so we need to call `forceUpdate` to re-render according to the new states.

Finally, to finish it off, we add the top bar. Create a file called `TopBar.js` in the `src` folder and add:

```
import React from "react";  
import Navbar from "react-bootstrap/Navbar";  
import Nav from "react-bootstrap/Nav";  
import NavDropdown from "react-bootstrap/NavDropdown";  
import "./TopBar.css";  
import { withRouter } from "react-router-dom";
function TopBar({ location }) {  
  const { pathname } = location; 
  return (  
    <Navbar bg="primary" expand="lg" variant="dark">  
      <Navbar.Brand href="#home">React Canvas App</Navbar.Brand>  
      <Navbar.Toggle aria-controls="basic-navbar-nav" />  
      <Navbar.Collapse id="basic-navbar-nav">  
        <Nav className="mr-auto">  
          <Nav.Link href="/" active={pathname == "/"}>  
            Home  
          </Nav.Link>  
        </Nav>  
      </Navbar.Collapse>  
    </Navbar>  
  );  
}

export default withRouter(TopBar);
```

The `Navbar` component is provided by React Boostrap.

After all the work is done, we get a whiteboard that we can draw on.