Categories
JavaScript React

Use React Refs to Manipulate the DOM and Konva to Add Graphics

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 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.

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 KonvaTransformer 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.

Categories
JavaScript

Using AJAX and JSON in JavaScript

AJAX makes our page dynamic and lets us refresh data in our web pages without refreshing the page. It makes user pages interactive and creates a slicker user experience for the user. AJAX stands for Asynchronous JavaScript and XML. It’s used to describe the way that we use the XMLHttpRequest object to refresh part of the page by getting data from the server via HTTP requests and manipulate the DOM to refresh the data.

The HTML DOM changes the dynamically. AJAX allows us to use the XMLHttpRequest object to get data from the server and manipulate the DOM without blocking the execution of other parts of the JavaScript program. Despite that AJAX stands for Asynchronous JavaScript and XML, JSON is frequently used for sending and retrieving data from the server. JSON stands for JavaScript Object Notation. JSON is a data format that very close to a JavaScript object, except that it can’t contain any functions or dynamic code. Only string, numbers, arrays and objects without functions are allowed.

Old style web pages refresh the whole page to refresh the data. This is getting less and less common today as it creates a less pleasant experience than just refreshing parts of the page that’s needed to refresh. Refreshing the whole page would make the whole page flash and everything has to be loaded again.

AJAX is also great for updating live data on a page since it only refreshes parts of a page that’s need to refresh. It’s good for tables, charts, emails, and things that refresh periodically.

To view AJAX in action, we can look at the browser’s development console. For example, in Chrome, we can go to a website like weather.com and then we can press F12 or right click the browser window and click Inspect to open the development console. Then we can go to the Network tab. In there, we have the XHR button, XHR stands for XmlHttpRequest, which is what’s originally used for AJAX. On the left side of the XHR section, click on one of the entries. Next click on Preview, and then we can see something like the following:

This is the JSON data that’s parsed by the browser and can be inserted to the DOM of a web page. All AJAX is sending data via HTTP requests and then response will be obtained from the the server, then the data is populated on the page via manipulating the DOM. Most modern browsers work the same way and support AJAX.

Same-Origin Policy

In most cases, requests are made to a server that has a different domain from the server that has a different domain from the originating server. If we do that without setting some options, we will get an error since most browsers by default stop cross origin requests from being made without setting specific options on the server and the client. The purpose of this is to prevent browsers from downloading code that’s unknown to the browser. This policy applies everywhere, even on our local computer, since it’s can be used to compromise the security of our computers no matter where the data is downloaded from.

To allow requests to request between computers with different domains from a web browser. The server has to enable Cross-Origin Resource Sharing, or CORS for short. This has to be enabled on the server side. If we look at a cross origin HTTP request, we should see an OPTIONS request before the actual request is made and in we should see the following in the response headers:

Access-Control-Allow-Origin: \*

Alternatively, we can put the client and server side code in the same domain. This wouldn’t violate the same origin policy in browsers since the request from the browser is made from the same domain from the server.

Using the XMLHttpRequest Object

We can use the XMLHttpRequest object to make a request to a server and retrieve its response. This can be done with a simple example. In this example, we will load content from a text file and then put the content inside a page. Create a file called index.html and add:

<html>  
  <head>  
    <title>XMLHttpRequest</title>  
  </head>  
  <body>  
    <h1>XML HTTP Request</h1>  
    <button id="load">Load Data</button>  
    <div id="content"></div>  
    <script src="script.js"></script>  
  </body>  
</html>

The create a file called script.js and add:

const reqListener = response => {  
  const content = document.getElementById("content");  
  content.innerHTML = response.currentTarget.response;  
};

const loadData = () => {  
  const req = new XMLHttpRequest();  
  req.onload = reqListener;  
  req.open("get", "file.txt", true);  
  req.send();  
};

window.onload = () => {  
  const loadButton = document.getElementById("load");  
  loadButton.onclick = loadData;  
};

Then in file.txt we put:

Hello world.

In the above example, we created an instance of the XMLHttpRequest object to make a GET request to our server, which is a local web server that servers the file.txt file. We call the open function to start the request, The first argument is the HTTP request method, which can be get , post , put , patch or delete . The second argument is the URL or relative path to your server side resource. The third argument is for setting whether the HTTP request happens asynchronously. Usually, it should be true since we do not want to hold up other parts of our JavaScript code from loading. Also, we set the req.onload listener function to get the response from the server and then populate the result. In reqListener function, we get the response from the server when it’s available and then populate the content of the element with ID content with it. This happens after we call end send function on the req object.

Once we have these files we should get ‘Hello world’ when we click ‘Load Data’, we should get ‘Hello world.’ in our page.

This is the simplest case for making HTTP requests and then getting the result with AJAX. If we want to make requests that are more complex, then this is a very cumbersome way to do it. Fortunately, we have the Fetch API. to make a HTTP requests in our client side applications.

Promises

In JavaScript, a lot of code are asynchronous since it’s single threaded. To prevent holding up a program by code that finishes running in a indeterminate amount of time, we have to make a lot code asynchronous to prevent holding up the rest of the program from executing.

A promise is an object that represents an asynchronous process that finishes in an indeterminate amount of time. Before it’s executed then its status is pending. It can end in 2 states. If it’s successful, then the promise is resolved. A resolved promised is in the fulfilled state. Otherwise, it ends by by rejecting the promise. A promise is rejected if an error occurred. If an error occurred then the value that’s returned in the promise is ignored. In either case, when the promise finishes executing, a promise is in the settled status. That means settled status includes both fulfilled and error status.

The benefit of using promises for writing asynchronous code is that we can chain multiple promises together as if they are synchronous code. However, promises can only return other promises and they cannot return anything else. They are chained with the then function, which takes a callback function which has the resolved value when the promise is fulfilled. When an errors occurred they can be handled with the catch function at the end. To execute code no matter what the result of a promise is, we can chain the finally function to the end of a promise.

To run an array of promises, we can use the Promise.all function, which takes an array of promises.

An example of a promise would be something like:

const getTextPromise = new Promise((resolve, reject) => {  
  const xhr = new XMLHttpRequest();  
  xhr.open('GET', 'file.txt', true);  
  xhr.onload = (response) => resolve(response.currentTarget.responseText);  
  xhr.onerror = () => reject(xhr.statusText);  
  xhr.send();  
});

We wrapped the example we have above inside a promise and we resolve the response text instead of populating it in the DOM directly. To populate it in the DOM, we do:

getTextPromise  
.then(responseText =>{  
  const content = document.getElementById("content");  
  content.innerHTML = responseText;  
})  
.catch(error =>{  
  alert(error.toString());  
})

Make HTTP Requests with the Fetch API

The Fetch API makes use of promises to make asynchronous HTTP requests from the browser to a server. It provides us with an easy to use API for making requests. It lets us create HTTP requests with a Request object, then when the server makes a response, then we get a Response object. We make a request with the fetch method. The fetch method takes one mandatory, which includes all the data for making requests. Once the Response object is returned then we can massage it to get it to the form we want and then populate it in our web pages.

Also, it’s aware of concepts like CORS, so we can make cross origin requests if the server allows by setting some options within the argument of the fetch method. To use the fetch method for making HTTP request, we can do it with an example. If we want to make a form for letting users subscribe to your email list, we can write the following code. Create a file calledindex.html and add:

<html>  
  <head>  
    <title>Email List Subscribe Form</title>  
  </head>  
  <body>  
    <h1>Email List Subscribe Form</h1>  
    <form action="" name="nameForm" id="nameForm" method="post">  
      <label for="firstName">First Name: </label>  
      <input type="text" name="firstName" id="firstName" /><br />  
      <label for="lastName">Last Name: </label>  
      <input type="text" name="lastName" id="lastName" /><br />  
      <label for="email">Email: </label>  
      <input type="text" name="email" id="email" /><br />  
      <input type="submit" value="Subscribe" />  
    </form>  
    <script src="script.js"></script>  
  </body>  
</html>

to add the form. Then create a script.js file in the same folder and add:

const APIURL = "http://localhost:3000";
const subscribe = data => {  
  return fetch(`${APIURL}/subscribers\`, {  
    method: "POST",  
    mode: "cors",  
    cache: "no-cache",  
    headers: {  
      "Content-Type": "application/json"  
    },  
    body: JSON.stringify(data)  
  })
  .then(response => response.json());  
};

window.onload = () => {  
  const nameForm = document.forms.nameForm;  
  nameForm.method = "post";  
  nameForm.target = "\_blank";  
  nameForm.action = "";  
  nameForm.addEventListener("submit", e => {  
    e.preventDefault();  
    const firstName = document.getElementById("firstName").value;  
    const lastName = document.getElementById("lastName").value;  
    const email = document.getElementById("email").value;  
    let errors = [];  
    if (!firstName) {  
      errors.push("First name is required.");  
    } 

    if (!lastName) {  
      errors.push("Last name is required.");  
    } 

    if (!email) {  
      errors.push("Email is required.");  
    } 

    if (!/\[^@\]+@\[^\.\]+\..+/.test(email)) {  
      errors.push("Email is invalid.");  
    } if (errors.length > 0) {  
      alert(errors.join(" "));  
      return;  
    }  
    subscribe({  
      firstName,  
      lastName,  
      email  
    }).then(response => {  
      alert(\`${response.firstName} ${response.lastName} has subscribed\`);  
    });  
  });  
};

The subscribe function is where we used the fetch method. In there we pass in the URL of the server we are making the request to as the first argument. In the second argument, we set the method, which the HTTP request method for the request we want to make, which is POST. mode should be CORS since we’re making a cross domain request. We don’t want any request caching so we set cache to no-cache . In headers , we set Content-Type to application/json to make the requests data type send JSON. The body is the request body of the request, which we create a JSON object since we are sending JSON to the server. Finally, we return response.json() , which is a promise with the response if the request is successful sent back from the server and converted to JSON format.

To send something to the URL we point to we have to set up a web server. We first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{  
  "subscribers": []  
}

So we have the subscribers endpoints defined in the requests.js available.

Shorten Promise Code with Async and Await

The code above makes use of several promises. We have one for making the request, and then another one for converting it to JSON. Then finally, we make the alert at the end. With the then function, we make to put callback functions as an argument of all of our then functions. This makes the code long is we have lots of promises. Instead, we can use the async and await syntax to replace the then and its associated callbacks as follows. In script.js , we put:

const APIURL = "http://localhost:3000";
const subscribe = async data => {  
  const response = await fetch(`${APIURL}/subscribers`, {  
    method: "POST",  
    mode: "cors",  
    cache: "no-cache",  
    headers: {  
      "Content-Type": "application/json"  
    },  
    body: JSON.stringify(data)  
  });  
  return response.json();  
};

window.onload = () => {  
  nameForm.method = "post";  
  nameForm.target = "_blank";  
  nameForm.action = "";  
  nameForm.addEventListener("submit", async e => {  
    e.preventDefault();  
    const firstName = document.getElementById("firstName").value;  
    const lastName = document.getElementById("lastName").value;  
    const email = document.getElementById("email").value;  
    let errors = \[\];  
    if (!firstName) {  
      errors.push("First name is required.");  
    } 

    if (!lastName) {  
      errors.push("Last name is required.");  
    } 

    if (!email) {  
      errors.push("Email is required.");  
    } 
    
    if (!/\[^@\]+@\[^\.\]+\..+/.test(email)) {  
      errors.push("Email is invalid.");  
    } 

    if (errors.length > 0) {  
      alert(errors.join(" "));  
      return;  
    }  
    try {  
      const response = await subscribe({  
        firstName,  
        lastName,  
        email  
      });  
      alert(`${response.firstName} ${response.lastName} has subscribed\`);  
    } catch (error) {  
      alert(error.toString());  
    }  
  });  
};

We replaced the then and callbacks with await . Then we can assign the resolved values of each promise as variables. Note that if we use await for our promise code then we have to put async in the function signature like we did in the above example. To catch errors, instead of chaining the catch function at the end, we use the catch clause to do it instead. Also, instead of chaining the finally function at the bottom to run code when a promise ends, we use the finally clause after the catch clause to do that instead.

async functions always return promises, and cannot return anything else like any other function that uses promises.

With the Fetch API and async and await syntax, we can make HTTP requests to servers much more easily than using the old XMLHttpRequest object. This is why now more and more web applications are using AJAX instead of refreshing the whole page on server side to get data.

Categories
JavaScript

A Guide to JSON and How It’s Handled in JavaScript

JSON stands for JavaScript Object Notation. It is a format for serializing data, which means that it can be used to transmit and receive data between different sources. In JavaScript, there’s a JSON utility object that provides methods to convert JavaScript objects to JSON strings and vice versa. The JSON utility object can’t be constructed or called — there are only 2 static methods which are stringify and parse to convert between JavaScript objects and JSON strings.

Properties of JSON

JSON is a syntax for serializing objects, arrays, numbers, booleans, and null. It is based on the JavaScript object syntax, but they are not the same thing. Not all JavaScript object properties can be converted to valid JSON, and JSON strings must be correctly formatted to be converted into a JavaScript object.

For objects and arrays, JSON property names must be in double-quoted strings, and trailing commas for objects are prohibited. Numbers cannot have leading zeroes, and a decimal point must be followed by at least one digit. NaN and Infinity aren’t supported, and JSON strings can’t have undefined or comments. In addition, JSON can not contain functions.

Any JSON text must contain valid JavaScript expressions. In some browser engines, the U+2028 line separator and U+2029 paragraph separator are allowed in string literals and property keys in JSON, but when using them in JavaScript code will result in SyntaxError. Those 2 characters can be parsed with JSON.parse into valid JavaScript strings, but fails when passed into eval.

Insignificant whitespace may be included anywhere except within JSONNumber or JSONString. Numbers can’t have whitespace inside and strings would be interpreted as whitespace in the string or cause an error. The tab character (U+0009), carriage return (U+000D), line feed (U+000A), and space (U+0020) characters are the only valid whitespace characters in JSON.

Basic Usage of the JSON Object

There are 2 methods on the JSON utility object. There is the stringify method for converting a JavaScript object to a JSON string and the parse method for converting a JSON string to a JavaScript object.

The parse method parses a string as JSON with a function as a second argument to optionally transform JSON entities to the JavaScript entity that you specified and return the resulting JavaScript object. If the string has entities that aren’t allowed in the JSON syntax, then a SyntaxError would be raised. Also, tailing commas aren’t allowed in the JSON string that is passed into JSON.parse. For example, we can use it as in the following code:

JSON.parse('{}'); // {}       
JSON.parse('false'); // false        
JSON.parse('"abc"'); // 'abc'         
JSON.parse('[1, 5, "abc"]');  // [1, 5, 'abc']  
JSON.parse('null'); // null

The first line would return an empty object. The second would return false. The third line would return 'abc'. The fourth line would return [1, 5, "abc"]. The fifth line would return null. It returns what we expect since every line we pass in is valid JSON.

Customize the Behavior of Stringify and Parse

Optionally, we can pass in a function as the second argument to convert values to whatever we want. The function we pass in will take the key as the first parameter and the value as the second and returns the value after manipulation is done. For example, we can write:

JSON.parse('{"a:": 1}', (key, value) =>  
  typeof value === 'number'  
    ? value * 10  
    : value       
);

Then we get {a: 10} returned. The function returns the original value multiplied by 10 if the value’s type is a number.

The JSON.stringify method can take a function as the second parameter that maps entities in the JavaScript object to something else in JSON. By default, all instances of undefined and unsupported native data like functions are removed. For example, if we write the following code:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc'  
}  
const jsonString = JSON.stringify(obj);  
console.log(jsonString);

Then we see that fn1 is removed from the JSON string after running JSON.stringify since functions aren’t supported in JSON syntax. For undefined, we can see from the following code that undefined properties will be removed.

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined  
}  
const jsonString = JSON.stringify(obj);  
console.log(jsonString);

undefinedProp is not in the JSON string logged because it has been removed by JSON.strinfiy.

Also, NaN and Infinity all become null after converting to a JSON string:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity  
}  
const jsonString = JSON.stringify(obj);  
console.log(jsonString);

We see that:

'{“foo”:1,”bar”:2,”abc”:”abc”,”nullProp”:null,”notNum”:null,”infinity”:null}'

NaN and Infinity have both become null instead of the original values.

For unsupported values, we can map them to supported values with the replacer function in the second argument which we can optionally pass in. The replace function takes the key of a property as the first parameter and the value as the second parameter. For example, one way to keep NaN, Infinity, or functions is to map them to a string like in the following code:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity  
}

const replacer = (key, value) => {  
  if (value instanceof Function) {  
    return value.toString();  
  } 
  else if (value === NaN) {  
    return 'NaN';  
  } 
  else if (value === Infinity) {  
    return 'Infinity';  
  } 
  else if (typeof value === 'undefined') {  
    return 'undefined';  
  } 
  else {  
    return value; // no change  
  }  
}

const jsonString = JSON.stringify(obj, replacer, 2);  
console.log(jsonString);

After running console.log on jsonString in the last line, we see that we have:

{  
  "fn1": "fn1() {}",  
  "foo": 1,  
  "bar": 2,  
  "abc": "abc",  
  "nullProp": null,  
  "undefinedProp": "undefined",  
  "notNum": null,  
  "infinity": "Infinity"  
}

What the replace function did was add additional parsing using the key and the value from the object being converted with JSON.stringify. It checks that if the value is a function, then we convert it to a string and return it. Likewise, with NaN, Infinity, and undefined, we did the same thing. Otherwise, we return the value as-is.

The third parameter of the JSON.stringfy function takes in a number to set the number of whitespaces to be inserted into the output of the JSON to make the output more readable. The third parameter can also take any string that will be inserted instead of whitespaces. Note that if we put a string as the third parameter that contains something other than white space(s), we may create a “JSON” a string that is not valid JSON.

For example, if we write:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity  
}
const replacer = (key, value) => {  
  if (value instanceof Function) {  
    return value.toString();  
  } 
  else if (value === NaN) {  
    return 'NaN';  
  } 
  else if (value === Infinity) {  
    return 'Infinity';  
  } 
  else if (typeof value === 'undefined') {  
    return 'undefined';  
  } 
  else {  
    return value; // no change  
  }  
}
const jsonString = JSON.stringify(obj, replacer, 'abc');  
console.log(jsonString);

Then console.log will be:

{  
abc"fn1": "fn1() {}",  
abc"foo": 1,  
abc"bar": 2,  
abc"abc": "abc",  
abc"nullProp": null,  
abc"undefinedProp": "undefined",  
abc"notNum": null,  
abc"infinity": "Infinity"  
}

Which is obviously not valid JSON. JSON.stringify will throw a “cyclic object value” TypeError. Also, if an object has BigInt values, then the conversion will fail with a “BigInt value can’t be serialized in JSON” TypeError.

Also, note that Symbols are automatically discarded with JSON.stringify if they are used as a key in an object. So if we have:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity,  
  [Symbol('foo')]: 'foo'  
}

const replacer = (key, value) => {
  if (value instanceof Function) {  
    return value.toString();  
  } 
  else if (value === NaN) {  
    return 'NaN';  
  } 
  else if (value === Infinity) {  
    return 'Infinity';  
  } 
  else if (typeof value === 'undefined') {  
    return 'undefined';  
  } 
  else {  
    return value; // no change  
  }  
}

const jsonString = JSON.stringify(obj, replacer, 2);  
console.log(jsonString);

We get back:

{  
  "fn1": "fn1() {}",  
  "foo": 1,  
  "bar": 2,  
  "abc": "abc",  
  "nullProp": null,  
  "undefinedProp": "undefined",  
  "notNum": null,  
  "infinity": "Infinity"  
}

Date objects are converted to strings by using the same string as what date.toISOString() will return. For example, if we put:

const obj = {  
  fn1() {},  
  foo: 1,  
  bar: 2,  
  abc: 'abc',  
  nullProp: null,  
  undefinedProp: undefined,  
  notNum: NaN,  
  infinity: Infinity,  
  [Symbol('foo')]: 'foo',  
  date: new Date(2019, 1, 1)  
}  
const replacer = (key, value) => {  
  if (value instanceof Function) {  
    return value.toString();  
  } 
  else if (value === NaN) {  
    return 'NaN';  
  } 
  else if (value === Infinity) {  
    return 'Infinity';  
  } 
  else if (typeof value === 'undefined') {  
    return 'undefined';  
  } 
  else {  
    return value; // no change  
  }  
}  
const jsonString = JSON.stringify(obj, replacer, 2);  
console.log(jsonString);

We get:

{  
  "fn1": "fn1() {}",  
  "foo": 1,  
  "bar": 2,  
  "abc": "abc",  
  "nullProp": null,  
  "undefinedProp": "undefined",  
  "notNum": null,  
  "infinity": "Infinity",  
  "date": "2019-02-01T08:00:00.000Z"  
}

As we can see, the value of the date property is now a string after converting to JSON.

Deep Copy Objects

We can also use JSON.stringify with JSON.parse to make a deep copy of JavaScript objects. For example, to do a deep copy of a object without a library, you can JSON.stringify then JSON.parse:

const a = { foo: {bar: 1, {baz: 2}}  
const b = JSON.parse(JSON.stringfy(a)) // get a clone of a which you can change with out modifying a itself

This does a deep copy of an object, which means all levels of an object are cloned instead of referencing the original object. This works because JSON.stringfy converted the object to a string which are immutable, and a copy of it is returned when JSON.parse parses the string which returns a new object that doesn’t reference the original object.

Categories
JavaScript Mistakes

Common JavaScript Mistakes — Part 1

JavaScript is a language that’s friendlier than many other programming languages in the world. However, it’s still very easy to make mistakes when writing JavaScript code through misunderstanding or overlooking stuff that we already know. By avoiding some of the mistakes below, we can make our lives easier by preventing bugs and typos in our code that bog us down with unexpected results.


Mismatching Brackets

Statements and functions nested in each other means that there’re multiple levels of brackets in each file. Usually, apps are pretty complex, so the levels can add up. This means that mismatching brackets is easy if you use a text editor that doesn’t support syntax highlight or don’t check for mismatched brackets. This can easily be avoided with modern text editors such as Visual Studio Code, Atom, and Sublime. If we want to use simpler text editors, then use linters and code formatting tools like ESLint and Prettier to detect these issues. They also let us format code automatically and detect common style issues that may occur, like quote style inconsistencies, number of characters in a line, functions that can be shortened, etc.


Mismatching Quotes and Parentheses

JavaScript lets us use single quotes, double quotes, and backticks for strings. They are equivalent. However, we should open and close with the same character. So if we open a string with a single quote, then use a single quote to close a string. If we start with double quotes or backticks, then use those respectively to close the string. Also, special characters like quotation marks have to escape to be included in a string in some cases. If you open a string with single quotes and you also use single quotes in a string, then you have to escape that to include them in the string. This also applies to double quotes and backticks. If you use double quotes in a double-quoted string, then you have to escape it. And if you use a backtick in a template string, then you have to escape the backtick character.

In if statements, parentheses always have to wrap around the whole condition. For example, something like

if (x > y) && (y < 10) {...}

won’t work. The correct way to write that statement is to write is

if ((x > y) && (y < 10)) {...}

if we want to check that both conditions are true.

We can easily avoid this by using a JavaScript code-aware text editor, like Visual Studio code, which will highlight these syntax errors for us so that we can fix these mistakes and make the code run.


Confusing the =, ==, and === Operators

The single equal (=) operator is for assigning the data on the right side to the variable on the left side. It should not be confused with the double equals (==) and the triple equals operator (===), which are operators for comparing values of the left and the right of the operator. In JavaScript we can use all three operators in an if statement. However, in most cases, we don’t mean to use the single equal operator inside the condition of the if statement. What we actually want is to use the double or triple equals operator to compare the operands on the left and right of it.

For example, we shouldn’t be writing

if (x = 1){ ... }

since we don’t want to assign 1 to x inside the if statement. Instead, we should be using the double or triple equals operator like the code below

if (x == 1){ ... }

or

if (x === 1){ ... }

The first example, if (x = 1){ … }, is always true, since x is truthy because it’s assigned the value of 1, which is truthy. What we actually want is to make a comparison which makes use of the conditional if statement.


Using Variables Outside of Block Level

If we use the var keyword to declare a variable, it can be referenced in any place below the var expression. For example, suppose we have:

for (var j = 0; j < 10; j++) {  
  j = j + 1;  
}  
console.log(j);

Then we can see that j is 10 when we run the console.log statement on the last line of the code. This is the reason that we have the let and const keywords to declare variables and constants. They’re blocked scoped so that they can’t be referenced outside the block. This means that we avoid bugs that can occur when we use var since the variable can’t be accessed outside the block that it’s declared in. As a result, we can’t assign it to anything else accidentally, causing issues with our code. Instead of using var like we did in the example above, we can use let like in the following example:

for (let j = 0; j < 10; j++) {  
  j = j + 1;  
}  
console.log(j);

We should get ReferenceError: j is not defined when we run the code above, which is a good sign since we don’t want j to be referenced outside the for loop. If we remove the console.log statement, then it will run.


Treating Array-Like Iterable Objects Like Arrays

In JavaScript, we can have properties with keys that are numbers. They’re automatically converted to strings before being accessed since they’re actually strings with all numerical content. Objects like the arguments and NodeList objects aren’t arrays, but they have properties stored with integer keys like they’re arrays. It’s easy to mistake them for arrays. The difference between arrays and array-like objects is that array-like objects aren’t arrays, but they both have a function with the Symbol Symbol.Iterator as the identifier. In these cases, we can convert them to arrays. For example, if we want to get the arguments passed into a regular function with the arguments object, we write something like the following with the spread operator:

function f() {  
  const args = [...arguments];  
  console.log(args);  
}  
f(1, 2, 3);

If we run the code above, we get [1,2,3].


Confusing Non-Array Objects With Arrays

Since we can access object properties and array entries with the bracket notation, which looks the same when used for arrays and regular objects, it’s easy to confuse objects and arrays.

For example, suppose we have the following code:

const obj = {  
  0: 1,  
  1: 2,  
  2: 3  
};  
console.log(obj[0]);

We see that the console.log statement would return 1. The code above has a regular object, but in the console.log statement, we pass in 0 as the key with the bracket notation to get the value obj[0], which is 1.

If we have an array and we try to access an entry by its index like in the code below:

const arr = [1, 2, 3];  
console.log(arr[0]);

we also get 1 with the console.log statement. They both use the bracket notation to access their values, but they aren’t the same. Arrays are objects, but you can iterate through them, unlike regular objects. If you try to loop through an array with the for...of loop or the forEach function, or try to use the spread operator with it, the example with the obj object will result in an error since it’s not an iterable object. We can make it iterable by adding a generator function with the Symbol Symbol.iterator to it like in the following code:

const obj = {  
  0: 1,  
  1: 2,  
  2: 3,  
  [Symbol.iterator]: function*() {  
    for (let prop in this) {  
      yield this[prop];  
    }  
  }  
};

Then when we iterate the obj object with the for...of loop like the code below:

for (let num of obj) {  
  console.log(num);  
}

we get back the entries of the new obj object that we made iterable.

The spread operator would also work. If we have the following code:

console.log([...obj]);

we get [1, 2, 3] from the console.log output.


Most of these tricks to avoid mistakes include features from ES6. It’s a major reason why ES6 was released. Generators and the let and const keywords are all part of the specification. The use of ES6, along with better text editors like Visual Studio Code, help us avoid mistakes that we would otherwise would make with older technologies. It’s a great reason to keep JavaScript code and tooling up to date. ES6 was released in 2015, so there’s no reason to stay back. For older browsers like Internet Explorer, we can use something like Babel to convert it to ES5 on the fly. Or we can use a module bundler like Webpack to convert ES6 or later code to ES5 in the build artifact so that we can write code with newer versions of JavaScript.

Categories
JavaScript

Using Arrow Functions in JavaScript

Arrow functions are a new type of functions introduced with ES2015 that provides an alternative compact syntax to functions declares with the function keyword. They don’t have their own bindings to this, argument, super or new.target keywords. This means that they aren’t suited for methods since they don’t bind to this, argument, or super. This also means that they can’t be used as constructors.

Declare Arrow Functions

To declare arrow functions, we can write the following if the function is one line long:

() => 2

The function above returns the number 2. Notice that for functions that are only one line long, we don’t need the return keyword to return anything. Whatever is at the end is returned. Also notice that if it’s has no parameters than we need to put empty parentheses for the function signature. If there’s one parameter, then we can write something like:

x => x*2

The function above takes in a parameter x and multiplies x by 2 and then return it. Notice that for arrow functions that only has one parameter, we don’t need to wrap it inside parentheses. If we have more than 1 parameter, then we need parentheses around the parameters, like in the following code:

(x,y) => x*y

In the function above, we take x and y as parameters and return x*y . If we want to return an object literal in a one-line arrow function, we have to put parentheses around the object literal that we want to return. For example, we write something like:

() => ({ a: 1, b: 2 });

to return the object { a: 1, b: 2 } .

If we need to define a function that spans multiple lines, then we need to wrap curly brackets around the function’s code as we have below:

(x, y) => {  
  return x+y;  
}

So the general syntax for arrow functions are one of the following:

(p1, p2, …, pN) => { statements } (p1, p2, …, pN) => expression  
  
(p) => { statements }p => { statements }  
  
() => { statements }

Default Parameters

Arrow functions’ parameters can have default values set for them so that we can kip them when are calling functions and still get a value set to them. Default parameters is easy with JavaScript. To set them in our code, we write the following:

const sum = (a,b=1) => a+b

In the code above, we set the default parameter value of b to 1, so if the second argument isn’t passed in when we call the sum function, then b is automatically set to 1. So if we call sum as follows:

sum(1)

we get 2 returned.

As we can see, now we don’t have to worry about having optional arguments being undefined , which would be the alternative result if no default value if set for parameters. This eliminates many sources of errors that occurs when making parameters optional with JavaScript.

The alternative way without using default parameters would be to check if each parameter is undefined . We write the following to do this:

const sum = (a,b,c)=>{  
  if (typeof b === 'undefined'){  
    b = 1;  
  }if (typeof c === 'undefined'){  
    c = 1;  
  }  
  return a+b+c;  
}

If we call the following sum function without some parameters, then we get:

const sum = (a,b = 1,c = 1)=> a+b+c;  
sum(1) // returns 3  
sum(1, 2) // returns 4

As we can see, we handled missing parameters gracefully with default parameters.

For default parameters, passing in undefined is the same as skipping the parameters. For example, if we call the following function and pass in undefined , then we get:

const sum = (a,b = 1,c = 1)=> a+b+c;  
sum(1,undefined) // returns 3  
sum(1,undefined,undefined) // returns 3  
sum(1, 2,undefined) // returns 4

Note that undefined is the same as skipping parameters. This isn’t true for other falsy values that are passed into a function. So if we pass in 0, null , false , or empty string, then they get passed in, and the default parameter values will be overwritten. For example, if we have the code below:

const test = (num=1) => typeof num;  
test(undefined);  
test(null);  
test('');  
test(0);  
test(false);

We get number for test(undefined) , object for test(null) , string for test(string) , number for test(0) , and boolean for test(false) . This means that anything other than undefined is passed in, but note that if we pass in falsy values and then run arithmetic operations on them, then falsy values get converted to 0.

const sum = (a,b = 1,c = 1)=> a+b+c;  
sum(1,null)  
sum(1,null,false)  
sum(1, 2,'')

So sum(1, null) returns 2 since b is converted to 0 and c has the default value of 1. sum(1) returns 1 since b and c are converted to 0. sum(1, 2,’’) is 3 since a is 1, b is passed in so that it becomes 2 instead of getting the default value of 1, and c is an empty string which is converted to 0.

Default arguments are evaluated at call time so that they’re set each time they’re called if no argument is passed into the function parameter with default values. For example, if we have:

const append = (val, arr = [])=>{  
  arr.push(val);  
  return arr;  
}
append(1);  
append(2);

append(1) returns [1] and append(2) returns [2] since we didn’t pass in an array to each function call, so arr is set to an empty array each time it’s run.

It’s also important to know that we can pass in values returned by functions in the default parameter, so if we a function that returns something, we can call it in the default parameter and assign the returned value to the parameter. For example, we can write:

const fn = () => 2  
const sum(a, b = fn()) => a+b;

Then if we call sum(1) , we get 3 since fn function returns 2. This is very handy if we want to manipulate and combine values beforehand before assigning it as a parameter.

Another great feature of the default parameters is that the parameters left of the given parameter is available for the parameter for assignment as default values, so we can have a function like the function below:

const saySomething = (name, somethingToSay, message = \`Hi ${name}. ${somethingToSay}`) => ({  
  name,  
  somethingToSay,  
  message  
});

Notice that we assigned an expression to the message parameter in the saySomething function. This is great for manipulating data and then assigning as we did before by assigning the function. We can also see that we can have default parameters that depend on parameters that are to the left of it. This means that default parameters to not have to be static.

So we call it with the first 2 parameters filled in, like saySomething(‘Jane’, ‘How are you doing?’). We get:

{name: "Jane", somethingToSay: "How are you doing?", message: "Hi Jane. How are you doing?"}

The message is returned with the template string that we defined evaluated.

We cannot call functions nested inside a function to get the returned value as the default parameter value. For example, if we write:

const fn = (a = innerFn()) => {  
  const innerFn = () => { return 'abc'; }  
}

This will result in a ReferenceError being thrown because the inner function isn’t defined yet when the default parameter is defined.

We can also have default parameter values that are set to the left of the required parameters. Arguments are still passed into parameters from the left to the right, so is we have:

const sum = (a=1,b) => a+b

If we have sum(1) we have NaN return 1 is added to undefined since we didn’t pas in anything for the second argument, b is undefined . However, if we write sum(1,2) then 3 is returned since we have a set to 1 and b set to 2.

Finally, we can use destructuring assignment to set default parameter values. For example, we can write:

const sum = ([a,b] = [1,2], {c:c} = {c:3}) => a+b+c;

Then we call sum without arguments we get 6 since a is set to 1, b is set to 2, and c is set to 3 by the destructuring assignment feature that JavaScript provides.

This Object

If this is referenced in a regular function declared with the function keyword, the this object isn’t set inside an arrow function to the function that has the this inside. If an arrow function is inside a constructor, then this is set to the new object. If it’s not inside any object, then this inside the arrow function is undefined in strict mode. this will be set to the base object if the arrow function is inside an object. However, we always get the window object if we reference this in an arrow function. For example, if we log this inside an arrow function that’s not inside a constructor or class like in the following code:

const fn = () => thisconsole.log(fn);

We get the window object logged when console.log is run. Likewise, if we ran:

let obj = {}  
obj.f = () => {  
  return this;  
};  
console.log(obj.f());

We get the same thing as we got before. This is in contrast to the tradition functions declared with the function keyword. If we replace the functions above with traditional functions in the code above, like in the following code:

const fn = function() {  
  return this  
};  
console.log(fn);

We get the function fn that we declared logged for fn.

However, if the arrow function is inside a constructor, we get the object that it’s the property of as the value of this. For example, if we have:

const obj = function() {  
  this.fn = () => this;  
}
console.log(new obj().fn())

Then this will be the current object the this.fn function is in, which is obj, so obj will be logged by console.log(new obj().fn()) . Also, if we have the code below:

let obj = {}  
obj.f = function() {  
  return this;  
};  
console.log(obj.f());

We get the obj object logged when it’s run.

When arrow functions are called with call and apply , the argument for the this object will be ignored. This means that they can’t be used to change the this object. For example, if we have:

function add(a) {  
  return this.base + a;  
}  
console.log(add.call({  
  base: 1  
}, 2));

We get 3 logged. This means that we can pass in an object that we want for this and get its value with traditional functions. This wouldn’t work with arrow functions. If we have:

const add = (a) => {  
  return this.base + a;  
}  
console.log(add.call({  
  base: 1  
}, 2));

We get NaN because this is window, which doesn’t have the base property. The first argument for call is ignored. Likewise, if we try to set the value of this inside the function when we use apply, we can do that with traditional functions:

function add(a) {   
  return this.base + a;  
}  
console.log(add.apply({  
  base: 1  
}, [1]));

We get 2 logged since the first argument for apply is the this object for the function inside. This means that we can change this.base to 1 by passing in our own object for the first argument of apply. When we try the same thing with arrow functions like in the following code:

const add = (a) => {   
  return this.base + a;  
}  
console.log(add.apply({  
  base: 1  
}, [1]));

We get NaN because this.base is undefined . Again this is set to the window object since we have an arrow function, so this cannot be changed with the apply function.

The arguments object isn’t bound to an arrow function, so we can’t use the arguments object to get the arguments passed into a function call of an arrow function. If we have a traditional, we can get the arguments passed in with the arguments object like the following code:

function add(){  
  return [...arguments].reduce((a,b) => +a + +b, [])  
}  
console.log(add(1,2,3));

We get 6 logged in the last line. This is because the arguments object had all the arguments of the object in string form. So arguments would have '1' , '2' , and '3' listed. However, if we try to do that we an arrow function, we can’t get the sum of the arguments with the arguments object. For example, if we have:

const add = () => [...arguments].reduce((a,b) => +a + +b, [])  
  
console.log(add(1,2,3));

We get NaN because we didn’t get the arguments of the function call in the arguments object.

Arrow functions can’t be used as constructors, therefore we can’t instantiate it with thenew keyword. For example, if we write the following and run it:

const Obj = () => {};  
let obj = new Obj();

We would get a TypeError .

Also, arrow functions don’t have a prototype property, so when we log something like:

const Obj = () => {};  
console.log(Obj.prototype);

We get undefined . This means we can‘t inherit from an arrow function. The yield keyword can’t be used in an arrow’s functions’s body. Therefore, we can’t use arrow functions as generator functions.

Arrow functions are useful for times when we need to write a function more concisely than writing traditional functions and we don’t need to change the content of the this object inside the function. Also, we can’t use them for generator functions or constructor since they don’t have their own this object. They also don’t have the prototype property so they can’t be inherited from. Also, the arguments of an arrow function call cannot be retrieved from the arguments object, so we just get them explicitly from the parameters.