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
React

How to Add Form Validation to Your React App with Redux Form

We look at basic uses of Redux form.

Form validation is a frequently needed feature in web apps. React does not come with its own form validation since it’s supposed to be a view library that provides developers with code structure. Fortunately, developers have come up with many form validation solutions. One of them is Redux Form, located at https://redux-form.com/8.2.2/.

As its name suggests, Redux Form uses Redux for storing the form data. It requires its own reducer for storing a form’s data. The form component that you write connects to the built-in reducer to store the data. If you want to get initial data, you load the data in your Redux store, then Redux Form can retrieve it.

Form value checks and form validations errors are provided by you. You can get form values from the store when you need it. The form input components can be customized easily by passing our custom component into the Field component provided by Redux Form.

In this article, we will write an address book app to illustrate the use of Redux Form for form validation. We need will have a Redux store for storing the form data, the selected contact data when editing, and a list of contacts. To start, we will run Create React App by running npx create-react-app address-book to create the app.

After that is run, we add our own libraries. We need Axios for making HTTP requests, React Bootstrap for styling, React Redux and Redux for state management, React Router for routing, and of course, Redux Form for form validation.

We install them by running:

npm i axios react-bootstrap react-redux react-router-fom redux redux-form

With our libraries installed, we can start writing our app. We create all files in the src folder unless otherwise specified. To start, we create actionCreators.js in the src folder and add:

import { SET_CONTACTS, LOAD } from "./actions";

const setContacts = contacts => {
  return {
    type: SET_CONTACTS,
    payload: contacts
  };
};

const setCurrentContact = contact => {
  return {
    type: LOAD,
    data: contact
  };
};

export { setContacts, setCurrentContact };

These are the actions and payload that we dispatch to our Redux store. setContacts is for setting a list of contacts and setCurrentContact is for loading the contact for editing.

Next, we create actions.js and add:

const SET_CONTACTS = "SET_CONTACTS";
const LOAD = "LOAD";

export { SET_CONTACTS, LOAD };

These are the constants for the action types of our store dispatch actions. Next in App.js , we replace the existing code with:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
const history = createHistory();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Address Book App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route path="/" exact component={HomePage} />
      </Router>
    </div>
  );
}

export default App;

to add the React Bootstrap Navbar component to the top of our page. We also add the React Router route for the home page that we’ll create.

Next, we create ContactForm.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import PropTypes from "prop-types";
import { addContact, editContact, getContacts } from "./requests";
import { connect } from "react-redux";
import { setContacts, setCurrentContact } from "./actionCreators";
import { Field, reduxForm } from "redux-form";
import { renderInputField } from "./RenderInputField";
import { renderCountryField } from "./RenderCountryField";
import { getFormValues, isInvalid } from "redux-form";

const validate = values => {
  const errors = {};
  if (!values.firstName) {
    errors.firstName = "Required";
  }
  if (!values.lastName) {
    errors.lastName = "Required";
  }
  if (!values.city) {
    errors.city = "Required";
  }
  if (!values.address) {
    errors.address = "Required";
  }
  if (!values.region) {
    errors.region = "Required";
  }
  if (!values.postalCode) {
    errors.postalCode = "Required";
  } else {
    if (
      (values.country == "United States" &&
        !/^[0-9]{5}(?:-[0-9]{4})?$/.test(values.postalCode)) ||
      (values.country == "Canada" &&
        !/^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/.test(values.postalCode))
    ) {
      errors.postalCode = "Invalid postal code";
    }
  }

if (!values.phone) {
    errors.phone = "Required";
  } else {
    if (
      (values.country == "United States" || values.country == "Canada") &&
      !/^[2-9]d{2}[2-9]d{2}d{4}$/.test(values.phone)
    ) {
      errors.phone = "Invalid phone";
    }
  }

if (!/[^@]+@[^.]+..+/.test(values.email)) {
    errors.email = "Invalid email";
  }
  if (Number.isNaN(+values.age) || values.age < 0 || values.age > 200) {
    errors.age = "Age must be between 0 and 200";
  }
  if (!values.country) {
    errors.country = "Required";
  }
  return errors;
};

function ContactForm({
  edit,
  onSave,
  setContacts,
  contact,
  onCancelAdd,
  onCancelEdit,
  invalid,
  values,
  currentContact,
  ...props
}) {
  const handleSubmit = async event => {
    if (invalid) {
      return;
    }
    if (!edit) {
      await addContact(values);
    } else {
      await editContact(values);
    }
    const response = await getContacts();
    setContacts(response.data);
    onSave();
  };

return (
    <div className="form">
      <Form noValidate onSubmit={props.handleSubmit(handleSubmit.bind(this))}>
        <Field
          name="firstName"
          type="text"
          component={renderInputField}
          label="First Name"
        />

        <Field
          name="lastName"
          type="text"
          component={renderInputField}
          label="Last Name"
        />

        <Field
          name="address"
          type="text"
          component={renderInputField}
          label="Address"
        />

        <Field
          name="city"
          type="text"
          component={renderInputField}
          label="City"
        />

        <Field
          name="region"
          type="text"
          component={renderInputField}
          label="Region"
        />

        <Field name="country" component={renderCountryField} label="Country" />

        <Field
          name="postalCode"
          type="text"
          component={renderInputField}
          label="Postal Code"
        />

        <Field
          name="phone"
          type="text"
          component={renderInputField}
          label="Phone"
        />

        <Field
          name="email"
          type="email"
          component={renderInputField}
          label="Email"
        />

        <Field
          name="age"
          type="text"
          component={renderInputField}
          label="Age"
        />

        <Button type="submit" style={{ marginRight: "10px" }}>
          Save
        </Button>
        <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>
          Cancel
        </Button>
      </Form>
    </div>
  );
}

ContactForm.propTypes = {
  edit: PropTypes.bool,
  onSave: PropTypes.func,
  onCancelAdd: PropTypes.func,
  onCancelEdit: PropTypes.func,
  contact: PropTypes.object
};

const mapStateToProps = state => {
  return {
    contacts: state.contacts
  };
};

const mapDispatchToProps = dispatch => ({
  setContacts: contacts => dispatch(setContacts(contacts))
});

ContactForm = connect(state => ({
  values: getFormValues("syncValidation")(state),
  invalid: isInvalid("syncValidation")(state)
}))(ContactForm);

ContactForm = connect(
  mapStateToProps,
  mapDispatchToProps
)(ContactForm);

ContactForm = reduxForm({
  form: "syncValidation",
  validate,
  enableReinitialize: true
})(ContactForm);

ContactForm = connect(state => {
  return {
    initialValues: state.currentContact.data
  };
})(ContactForm);

export default ContactForm;

This is the form for editing and adding our contact. We use Redux Form extensively here. The Form component is provided by React Bootstrap. The Field component is from Redux Form. We pass in our custom input field components into the component prop of the field, along with the name , type , and label of our inputs. We will use them in our custom field components, renderInputField and renderCountryField .

In the onSubmit prop of the Form component, we wrap our own handleSubmit function with Redux Form’s handleSubmit function provided in the props so that we can use our own form submit handler instead of Redux Form’s handleSubmit function for handling form submissions.

In our handleSubmit function, we check for the form’s validity by getting the invalid prop from the props. The validation rules are in the validate function at the top of the code, which we passing into the reduxForm function at the bottom of our code. In the validate function, we get the form input values in the values parameter and set the errors object with the error messages of each field. We can check validation that depends on other fields easily in this function.

Once the form is checked to be valid, whereinvalid is false , then we call addContact or editContact by passing the value prop we get from the connect functions from Redux Form depending on if we are adding or not and save the data. Then we call getContacts and setContacts from mapDispatchToProps to update our Redux store with the latest contact entries from back end. Then call onSave , which is passed in from HomePage.js that we will create, to close the modal.

The invalid , and values props are generated by running:

ContactForm = connect(state => ({
  values: getFormValues("syncValidation")(state),
  invalid: isInvalid("syncValidation")(state)
}))(ContactForm);

This block:

ContactForm = reduxForm({
  form: "syncValidation",
  validate,
  enableReinitialize: true
})(ContactForm);

provides us with form validation capabilities of Redux Form in this component, and the initial values of the edit form is retrieved from our store by running:

ContactForm = connect(state => {
  return {
    initialValues: state.currentContact.data
  };
})(ContactForm);

Next, create exports.js and add:

export const COUNTRIES = [
  "Afghanistan",
  "Albania",
  "Algeria",
  "Andorra",
  "Angola",
  "Anguilla",
  "Antigua &amp; Barbuda",
  "Argentina",
  "Armenia",
  "Aruba",
  "Australia",
  "Austria",
  "Azerbaijan",
  "Bahamas",
  "Bahrain",
  "Bangladesh",
  "Barbados",
  "Belarus",
  "Belgium",
  "Belize",
  "Benin",
  "Bermuda",
  "Bhutan",
  "Bolivia",
  "Bosnia &amp; Herzegovina",
  "Botswana",
  "Brazil",
  "British Virgin Islands",
  "Brunei",
  "Bulgaria",
  "Burkina Faso",
  "Burundi",
  "Cambodia",
  "Cameroon",
  "Canada",
  "Cape Verde",
  "Cayman Islands",
  "Chad",
  "Chile",
  "China",
  "Colombia",
  "Congo",
  "Cook Islands",
  "Costa Rica",
  "Cote D Ivoire",
  "Croatia",
  "Cruise Ship",
  "Cuba",
  "Cyprus",
  "Czech Republic",
  "Denmark",
  "Djibouti",
  "Dominica",
  "Dominican Republic",
  "Ecuador",
  "Egypt",
  "El Salvador",
  "Equatorial Guinea",
  "Estonia",
  "Ethiopia",
  "Falkland Islands",
  "Faroe Islands",
  "Fiji",
  "Finland",
  "France",
  "French Polynesia",
  "French West Indies",
  "Gabon",
  "Gambia",
  "Georgia",
  "Germany",
  "Ghana",
  "Gibraltar",
  "Greece",
  "Greenland",
  "Grenada",
  "Guam",
  "Guatemala",
  "Guernsey",
  "Guinea",
  "Guinea Bissau",
  "Guyana",
  "Haiti",
  "Honduras",
  "Hong Kong",
  "Hungary",
  "Iceland",
  "India",
  "Indonesia",
  "Iran",
  "Iraq",
  "Ireland",
  "Isle of Man",
  "Israel",
  "Italy",
  "Jamaica",
  "Japan",
  "Jersey",
  "Jordan",
  "Kazakhstan",
  "Kenya",
  "Kuwait",
  "Kyrgyz Republic",
  "Laos",
  "Latvia",
  "Lebanon",
  "Lesotho",
  "Liberia",
  "Libya",
  "Liechtenstein",
  "Lithuania",
  "Luxembourg",
  "Macau",
  "Macedonia",
  "Madagascar",
  "Malawi",
  "Malaysia",
  "Maldives",
  "Mali",
  "Malta",
  "Mauritania",
  "Mauritius",
  "Mexico",
  "Moldova",
  "Monaco",
  "Mongolia",
  "Montenegro",
  "Montserrat",
  "Morocco",
  "Mozambique",
  "Namibia",
  "Nepal",
  "Netherlands",
  "Netherlands Antilles",
  "New Caledonia",
  "New Zealand",
  "Nicaragua",
  "Niger",
  "Nigeria",
  "Norway",
  "Oman",
  "Pakistan",
  "Palestine",
  "Panama",
  "Papua New Guinea",
  "Paraguay",
  "Peru",
  "Philippines",
  "Poland",
  "Portugal",
  "Puerto Rico",
  "Qatar",
  "Reunion",
  "Romania",
  "Russia",
  "Rwanda",
  "Saint Pierre &amp; Miquelon",
  "Samoa",
  "San Marino",
  "Satellite",
  "Saudi Arabia",
  "Senegal",
  "Serbia",
  "Seychelles",
  "Sierra Leone",
  "Singapore",
  "Slovakia",
  "Slovenia",
  "South Africa",
  "South Korea",
  "Spain",
  "Sri Lanka",
  "St Kitts &amp; Nevis",
  "St Lucia",
  "St Vincent",
  "St. Lucia",
  "Sudan",
  "Suriname",
  "Swaziland",
  "Sweden",
  "Switzerland",
  "Syria",
  "Taiwan",
  "Tajikistan",
  "Tanzania",
  "Thailand",
  "Timor L'Este",
  "Togo",
  "Tonga",
  "Trinidad &amp; Tobago",
  "Tunisia",
  "Turkey",
  "Turkmenistan",
  "Turks &amp; Caicos",
  "Uganda",
  "Ukraine",
  "United Arab Emirates",
  "United Kingdom",
  "United States",
  "United States Minor Outlying Islands",
  "Uruguay",
  "Uzbekistan",
  "Venezuela",
  "Vietnam",
  "Virgin Islands (US)",
  "Yemen",
  "Zambia",
  "Zimbabwe"
];

so we get an array of countries for our country field drop down.

Next in HomePage.css , we add:

.home-page {
    padding: 20px;
}

to get our page some padding.

Then we create HomePage.js and add:

import React from "react";
import { useState, useEffect } from "react";
import Table from "react-bootstrap/Table";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import ContactForm from "./ContactForm";
import "./HomePage.css";
import { connect } from "react-redux";
import { getContacts, deleteContact } from "./requests";
import { setCurrentContact } from "./actionCreators";

function HomePage({ setCurrentContact }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedContact, setSelectedContact] = useState({});
  const [contacts, setContacts] = useState([]);

  const openModal = () => {
    setOpenAddModal(true);
  };

  const closeModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
    getData();
  };

  const cancelAddModal = () => {
    setOpenAddModal(false);
  };

  const editContact = contact => {
    setSelectedContact(contact);
    setCurrentContact(contact);
    setOpenEditModal(true);
  };

  const cancelEditModal = () => {
    setOpenEditModal(false);
  };

  const getData = async () => {
    const response = await getContacts();
    setContacts(response.data);
    setInitialized(true);
  };

  const deleteSelectedContact = async id => {
    await deleteContact(id);
    getData();
  };

  useEffect(() => {
    if (!initialized) {
      getData();
    }
  });

  return (
    <div className="home-page">
      <h1>Contacts</h1>
      <Modal show={openAddModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm
            edit={false}
            onSave={closeModal.bind(this)}
            onCancelAdd={cancelAddModal}
          />
        </Modal.Body>
      </Modal>

      <Modal show={openEditModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm
            edit={true}
            onSave={closeModal.bind(this)}
            contact={selectedContact}
            onCancelEdit={cancelEditModal}
          />
        </Modal.Body>
      </Modal>
      <ButtonToolbar onClick={openModal}>
        <Button variant="outline-primary">Add Contact</Button>
      </ButtonToolbar>
      <br />
      <div className="table-responsive">
        <Table striped bordered hover>
          <thead>
            <tr>
              <th>First Name</th>
              <th>Last Name</th>
              <th>Address</th>
              <th>City</th>
              <th>Country</th>
              <th>Postal Code</th>
              <th>Phone</th>
              <th>Email</th>
              <th>Age</th>
              <th>Edit</th>
              <th>Delete</th>
            </tr>
          </thead>
          <tbody>
            {contacts.map(c => (
              <tr key={c.id}>
                <td>{c.firstName}</td>
                <td>{c.lastName}</td>
                <td>{c.address}</td>
                <td>{c.city}</td>
                <td>{c.country}</td>
                <td>{c.postalCode}</td>
                <td>{c.phone}</td>
                <td>{c.email}</td>
                <td>{c.age}</td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editContact.bind(this, c)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSelectedContact.bind(this, c.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </Table>
      </div>
    </div>
  );
}

const mapStateToProps = state => {
  return {
    contacts: state.contacts
  };
};

const mapDispatchToProps = dispatch => ({
  setCurrentContact: contact => dispatch(setCurrentContact(contact))
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(HomePage);

This is the home page of our app. We have a button to open the modal and a table to display the address book entries. Also, we have functions to open and close the modal with the openModal , closeModal , cancelAddModal , and cancelEditModal functions. We have a modal for add and another for editing contacts, they open the same form. When the edit modal is open with the editContact function, setCurrentContact provided by the mapDispatchToProps function via the props is run, setting the current contact being edited so that it can be retrieved for the initialValues of our ContactForm component by calling the connect function like we did. We have Edit and Delete buttons to call editContact and deleteSelectedContact respectively.

In index.js , replace the existing code with:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { createStore, combineReducers } from "redux";
import { reducer as formReducer } from "redux-form";
import { Provider } from "react-redux";
import { contactsReducer, currentContactReducer } from "./reducers";

const rootReducer = combineReducers({
  form: formReducer,
  contacts: contactsReducer,
  currentContact: currentContactReducer
});

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: [https://bit.ly/CRA-PWA](https://bit.ly/CRA-PWA)
serviceWorker.unregister();

to inject our Redux store so that it’s available throughout our app.

Then create reducers.js and add:

import { SET_CONTACTS, LOAD } from "./actions";

function contactsReducer(state = {}, action) {
  switch (action.type) {
    case SET_CONTACTS:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state;
  }
}

function currentContactReducer(state = {}, action) {
  switch (action.type) {
    case LOAD:
      return {
        data: action.data
      };
    default:
      return state;
  }
}

export { contactsReducer, currentContactReducer };

to create the 2 reducers that we mentioned before.

Next, create RenderCountryField.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import { COUNTRIES } from "./exports";

export const renderCountryField = ({
  input,
  label,
  type,
  meta: { touched, error, warning }
}) => (
  <Form.Row>
    <Form.Group as={Col} md="12" controlId="country">
      <Form.Label>{label}</Form.Label>
      <Form.Control
        as="select"
        type={type}
        placeholder={label}
        {...input}
        isInvalid={touched && error}
      >
        {COUNTRIES.map(c => (
          <option key={c} value={c}>
            {c}
          </option>
        ))}
      </Form.Control>
      <Form.Control.Feedback type="invalid">
        {touched && error}
      </Form.Control.Feedback>
    </Form.Group>
  </Form.Row>
);

to create our country drop-down with form validation, which we pass into the component prop of the Field component in ContactForm.js .

Next create RenderInputField.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";

export const renderInputField = ({
  input,
  label,
  type,
  meta: { touched, error, warning }
}) => (
  <Form.Row>
    <Form.Group as={Col} md="12">
      <Form.Label>{label}</Form.Label>
      <Form.Control
        type={type}
        placeholder={label}
        {...input}
        isInvalid={touched && error}
      />
      <Form.Control.Feedback type="invalid">
        {touched &&
          ((error && <span>{error}</span>) ||
            (warning && <span>{warning}</span>))}
      </Form.Control.Feedback>
    </Form.Group>
  </Form.Row>
);

to create the text input for our contact form.

Both inputs use the Form component from React Bootstrap.

Next, create requests.js and add:

const APIURL = 'http://localhost:3000';
const axios = require('axios');
export const getContacts = () => axios.get(`${APIURL}/contacts`);

export const addContact = (data) => axios.post(`${APIURL}/contacts`, data);

export const editContact = (data) => axios.put(`${APIURL}/contacts/${data.id}`, data);

export const deleteContact = (id) => axios.delete(`${APIURL}/contacts/${id}`);

to create the functions for making the HTTP requests to our back end to persist our contact data.

Finally, in index.html , replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React Address Book App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to change our app title and add our Bootstrap CSS.

Now we can run the app by running set PORT=3001 && react-scripts start on Windows or PORT=3006 react-scripts start on Linux.

To start the back end, 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:

{
  "contacts": [
  ]
}

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

Categories
JavaScript React

How To Use Hooks in React

The latest version of React (v16.8 or later) introduced Hooks, which allow us to set state more simply than before. It also makes function components smart, having the equivalent functionality and lifecycle as class-based components.

State management in a single-page app is also important. Without centralized state management, a lot of data has to be passed directly between components, which becomes unmaintainable and confusing very quickly.

Flux architecture remedies this by making apps store the state in a centralized location and the states are stored as immutable objects to prevent accidental modification.

Redux is one of the most popular libraries for state management and, with React Redux, it can connect your Redux store directly to your components, as you will see below.

In this piece, I will create an app with React and React Redux in the simplest way possible.

Create a New React App

Use the create-react-app code generator, created by the developers of React.

Here is the README and full documentation for create-react-app.

The app that will be created is an app that displays data from the Dog API.

To create the app, run npx create-react-app, and follow the instructions. This will create a new app.

Then, you are ready to install React Router. To install it, run npm i react-router-dom.

After that, install @material-ui/core and axios by running npm i @material-ui/core axios.

Material-UI provides the Material Design look to our app, and axios is an HTTP client which works in client-side apps.

In index.js, we have:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { breedsReducer, imagesReducer } from './reducers';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { combineReducers } from 'redux'

const dogApp = combineReducers({
  breeds: breedsReducer,
  images: imagesReducer
})

const store = createStore(dogApp)

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
, document.getElementById('root')
);

serviceWorker.unregister();

The file above is where the reducers are mapped to states.

As the combineReducers function is called, the store is created, which is then passed into the app, where the mapStateToProps will make the state available to the component as props.

The mapDispatchToProps allows you to set state in your component via a function in the props, as you will see below.

We add reducers to store state in a centrally available location. The states of our app are set here.

We create a file called reducer.js:

import { SET_BREEDS, SET_IMAGES } from './actions';

function breedsReducer(state = {}, action) {
   switch (action.type) {
    case SET_BREEDS:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state
   }
}

function imagesReducer(state = [], action) {
   switch (action.type) {
    case SET_IMAGES:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state
  }
}
export { breedsReducer, imagesReducer };

In actions.js, we add these constants for our Redux actions:

const SET_BREEDS = 'SET_BREEDS';
const SET_IMAGES = 'SET_IMAGES';export { SET_BREEDS, SET_IMAGES };

In actionCreators.js, we add:

import { SET_BREEDS, SET_IMAGES } from './actions';

const setBreeds = (breeds) => {
  return {
    type: SET_BREEDS,
    payload: breeds
  }
};

const setImages = (images) => {
  return {
    type: SET_IMAGES,
    payload: images
  }
};
export { setBreeds, setImages };

In app.js, we change the default code to:

import React, { useState, useEffect } from "react";
import './App.css';
import { setBreeds } from './actionCreators';
import { connect } from 'react-redux';
import { Router, Route, Link } from "react-router-dom";
import BreedsPage from './BreedsPage';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import { makeStyles } from '@material-ui/core/styles';
import { createBrowserHistory as createHistory } from 'history'
const history = createHistory();
const axios = require('axios');
const useStyles = makeStyles(theme => ({
  root: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  title: {
    flexGrow: 1,
  },
}));

function App({ setBreeds }) {
  const classes = useStyles();
  const [initialized, setInitialized] = useState(false);
  const [state, setState] = useState({
    openDrawer: false
  });   
  const titles = {
     '/': 'Dog App',
     '/breeds': 'Get Images By Breed - Dog App',
     '/subbreeds': 'Get Images By Breed or Sub-Breed - Dog App'
   }

   history.listen((location, action) => {
     document.title = titles[location.pathname];
     setState({ openDrawer: false });
   });

  const toggleDrawer = (open) => event => {
   if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
      return;
    }
    setState({ openDrawer: open });
   };

  const links = {
    'Home': '/',
    'Breeds': '/breeds',
    'Sub-Breeds': '/subbreeds',
   }

  const getBreeds = () => {
    setInitialized(true);
    axios.get('https://dog.ceo/api/breeds/list/all')
    .then((response) => {
      setBreeds(response.data.message);
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
    });
   }
  useEffect(() => {
     if (!initialized) {
       getBreeds();
     }
  });

  return (
      <div className="App">
        <Router history={history}>
          <Drawer anchor="left" open={state.openDrawer} onClose=     {toggleDrawer(false)}>
            <List>
               <ListItem button>
                 <h2><b>Dog App</b></h2>
               </ListItem>
               {Object.keys(links).map((text) => (
                  <ListItem button key={text}>
                    <Link to={links[text]}>
                      <ListItemText primary={text} />
                   </Link>
                </ListItem>
                ))}
            </List>
          </Drawer>
           <AppBar position="static">
             <Toolbar>
               <IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="Menu" onClick={toggleDrawer(true)}>
                 <i className="material-icons">menu</i>
               </IconButton>
               <Typography variant="h6" className={classes.title}>Dog App</Typography>
             </Toolbar>
          </AppBar>
          <Route path="/breeds/" component={BreedsPage} />
        </Router>
       </div>
      );
}

const mapStateToProps = (state) => ({
  breeds: state.breeds
})

const mapDispatchToProps = (dispatch) => ({
  setBreeds: breeds => dispatch(setBreeds(breeds))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

The code above has Hooks.

const [initialized, setInitialized] = useState(false);
const [state, setState] = useState({
  openDrawer: false
});

These functions are equivalent to setState functions in class-based components.

The first element in the array (initialized) is equivalent to this.state.initialized and setInitialized is equivalent to a function that calls this.setState({initialized: initializedValue}); in a class-based component.

Hooks only work with function-based components. The benefit is writing fewer lines of code to achieve the same effect of setting state.

Also, note that we have this in the above component:

useEffect(() => {
  if (!initialized) {
    getBreeds();
  }
});

As we don’t have componentDidMount, like we do in class-based components, we have to check if the component is loaded with our own flag.

The getBreeds function sets the initialized flag to true once it has run successfully so that the getBreeds function will not repeatedly run forever.

useEffect is a function that is run during every render, so be careful not to put necessary code in there.

Note the connect function at the end of the file above. This where the state connects to the component.

setBreeds is a function which returns a plain object with the action type and the payload. This allows the reducer to set the state according to the type field, which in this case would be SET_BREEDS or SET_IMAGES.

The state will be set, returned, and the new state will be available via props.breeds for breeds.

The <Route path=”/breeds/” component={BreedsPage} /> is where the route is defined. It must be inside <Router history={history}></Router>. This is the routing part of our application.

With this, we can go to the page with http://localhost:3000/breeds.

This block sets the title and hides the app drawer on the left when the route changes:

history.listen((location, action) => {
   document.title = titles[location.pathname];
   setState({ openDrawer: false });
});

We now create the pages for our app, which will be used by React Router for routing.

First, we create a page for displaying breeds, we will call it BreedPage.js.

The code will look like this:

import React from 'react';
import './BreedsPage.css';
import { setImages } from './actionCreators';
import { connect } from 'react-redux';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { makeStyles } from '@material-ui/core/styles';
import ImagesBox from './ImagesBox';
const axios = require('axios');
const useStyles = makeStyles(theme => ({
    formControl: {
        margin: theme.spacing(1),
        width: '90vw',
    },
}));

function BreedsPage({ breeds, setImages }) {
    const classes = useStyles();
    const [state, setState] = React.useState({
        breed: '',
    });

    const [initialized, setInitialized] = React.useState(false);
    const handleChange = name => event => {
        setState({
            ...state,
            [name]: event.target.value,
        });
        if (name == 'breed') {
            getImagesByBreed(event.target.value);
        }
    };

    const getImagesByBreed = (breed) => {
        axios.get(`https://dog.ceo/api/breed/${breed}/images`)
            .then((response) => {
                setImages(response.data.message);
            })
            .catch((error) => {
                console.log(error);
            })
            .finally(() => {
            });
    }

    React.useEffect(() => {
        if (!initialized) {
            setInitialized(true);
            setImages([]);
        }
    });
    return (
        <div className="App">
            <h1>Get Images By Breed</h1>
            <form>
                <FormControl className={classes.formControl}>
                    <InputLabel>Breed</InputLabel>
                    <Select
                        value={state.breed}
                        onChange={handleChange('breed')}
                    >
                        {Object.keys(breeds || {}).map(b =>
                            <MenuItem value={b} key={b}>
                                {b}
                            </MenuItem>
                        )}
                    </Select>
                </FormControl>
                <ImagesBox></ImagesBox>
            </form>
        </div>
    );
}

const mapStateToProps = state => {
    return {
        breeds: state.breeds,
        images: state.images
    }
}

const mapDispatchToProps = dispatch => ({
    setImages: images => dispatch(setImages(images))
})

export default connect(
    mapStateToProps,
    mapDispatchToProps
)(BreedsPage);

Categories
JavaScript React

How To Use Redux With React

If you want to make a single-page app with React, you need to add routing to your app. React Router is the most popular routing library for React apps. To use it, you have to add the library via npm.

In this piece, I will create an app with React and React Router in the simplest way possible.

To create a new React app, use the create-react-app code generator made by the developers of React. Here are the README and full documentation.

The app that will be created is an app that displays data from the Dog API.

To create the app, run npx create-react-app and follow the instructions, you will get a new app. Then, you are ready to install React Router.

To install it, run npm i react-router-dom. After that, install @material-ui/core and axios by running npm i @material-ui/core axios. Material UI provides a Material Design look to our app and Axios is an HTTP client which works at client-side apps.

In index.js, we have:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { breedsReducer, imagesReducer } from './reducers';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { combineReducers } from 'redux'
const dogApp = combineReducers({
  breeds: breedsReducer,
  images: imagesReducer
})
const store = createStore(dogApp)
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
, document.getElementById('root')
);
serviceWorker.unregister();

First, we add reducers to store state in a centrally available location. We make a file called reducer.js:

import { SET_BREEDS, SET_IMAGES } from './actions';
function breedsReducer(state = {}, action) {
   switch (action.type) {
    case SET_BREEDS:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state
   }
}
function imagesReducer(state = [], action) {
   switch (action.type) {
    case SET_IMAGES:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state
  }
}
export { breedsReducer, imagesReducer };

In actions.js, we add these constants for our Redux actions:

const SET_BREEDS = 'SET_BREEDS';
const SET_IMAGES = 'SET_IMAGES';
export { SET_BREEDS, SET_IMAGES };

InactionCreators.js, we add:

import { SET_BREEDS, SET_IMAGES } from './actions';
const setBreeds = (breeds) => {
  return {
    type: SET_BREEDS,
    payload: breeds
  }
};
const setImages = (images) => {
  return {
    type: SET_IMAGES,
    payload: images
  }
};
export { setBreeds, setImages };

In app.js, we change the default code to:

import React, { useState, useEffect } from "react";
import './App.css';
import { setBreeds } from './actionCreators';
import { connect } from 'react-redux';
import { Router, Route, Link } from "react-router-dom";
import BreedsPage from './BreedsPage';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import { makeStyles } from '@material-ui/core/styles';
import { createBrowserHistory as createHistory } from 'history'
const history = createHistory();
const axios = require('axios');
const useStyles = makeStyles(theme => ({
  root: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  title: {
    flexGrow: 1,
  },
}));
function App({ setBreeds }) {
  const classes = useStyles();
  const [initialized, setInitialized] = useState(false);
  const [state, setState] = useState({
    openDrawer: false
   });
   const titles = {
     '/': 'Dog App',
     '/breeds': 'Get Images By Breed - Dog App',
     '/subbreeds': 'Get Images By Breed or Sub-Breed - Dog App'
   }

   history.listen((location, action) => {
     document.title = titles[location.pathname];
     setState({ openDrawer: false });
   });
   const toggleDrawer = (open) => event => {
   if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
      return;
    }
    setState({ openDrawer: open });
   };
   const links = {
    'Home': '/',
    'Breeds': '/breeds',
    'Sub-Breeds': '/subbreeds',
   }
   const getBreeds = () => {
    setInitialized(true);
    axios.get('https://dog.ceo/api/breeds/list/all')
    .then((response) => {
      setBreeds(response.data.message);
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
    });
   }
   useEffect(() => {
     if (!initialized) {
       getBreeds();
     }
   });
   return (
      <div className="App">
        <Router history={history}>
          <Drawer anchor="left" open={state.openDrawer} onClose=       {toggleDrawer(false)}>
            <List>
               <ListItem button>
                 <h2><b>Dog App</b></h2>
               </ListItem>
               {Object.keys(links).map((text) => (
                  <ListItem button key={text}>
                    <Link to={links[text]}>
                      <ListItemText primary={text} />
                   </Link>
                </ListItem>
                ))}
            </List>
          </Drawer>
           <AppBar position="static">
             <Toolbar>
               <IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="Menu" onClick={toggleDrawer(true)}>
                 <i className="material-icons">menu</i>
               </IconButton>
               <Typography variant="h6" className={classes.title}>Dog App</Typography>
             </Toolbar>
          </AppBar>
          <Route path="/breeds/" component={BreedsPage} />
        </Router>
       </div>
      );
}
const mapStateToProps = (state) => ({
  breeds: state.breeds
})
const mapDispatchToProps = (dispatch) => ({
  setBreeds: breeds => dispatch(setBreeds(breeds))
})
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

The <Route path=”/breeds/” component={BreedsPage} /> is where the route is defined. It must be inside <Router history={history}></Router>.

This is the routing part of our application. With this, we can go to the page when we type in http://localhost:3000/breeds.

This block sets the title and hides the app drawer on the left when the route changes:

history.listen((location, action) => {
   document.title = titles[location.pathname];
   setState({ openDrawer: false });
});

We now create the pages for our apps which will use React Router for routing. First, we create a page for displaying breeds, we call it BreedPage.js.

The code will look like this:

import React from 'react';
import './BreedsPage.css';
import { setImages } from './actionCreators';
import { connect } from 'react-redux';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { makeStyles } from '@material-ui/core/styles';
import ImagesBox from './ImagesBox';
const axios = require('axios');
const useStyles = makeStyles(theme => ({
  formControl: {
    margin: theme.spacing(1),
    width: '90vw',
  },
}));
function BreedsPage({ breeds, setImages }) {
  const classes = useStyles();
  const [state, setState] = React.useState({
    breed: '',
  });
  const [initialized, setInitialized] = React.useState(false);
  const handleChange = name => event => {
    setState({...state, [name]: event.target.value,});
    if (name == 'breed') {
      getImagesByBreed(event.target.value);
    }
  };
  const getImagesByBreed = (breed) => {
    axios.get(`https://dog.ceo/api/breed/${breed}/images`)
    .then((response) => {
      setImages(response.data.message);
    })
    .catch((error) => {
      console.log(error);   
    })
    .finally(() => {
    });
  }
  React.useEffect(() => {
    if (!initialized) {
      setInitialized(true);
      setImages([]);
    }
  });
  return (
    <div className="App">
      <h1>Get Images By Breed</h1>
      <form>
       <FormControl className={classes.formControl}>
         <InputLabel>Breed</InputLabel>
         <Select
           value={state.breed}
           onChange={handleChange('breed')}
         >
           {Object.keys(breeds || {}).map(b =>
             <MenuItem value={b} key={b}>{b}</MenuItem>
           )}
         </Select>
       </FormControl>
       <ImagesBox></ImagesBox>
      </form>
    </div>
  );
}
const mapStateToProps = state => {
  return {
    breeds: state.breeds,
    images: state.images
  }
}
const mapDispatchToProps = dispatch => ({
  setImages: images => dispatch(setImages(images))
})
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(BreedsPage);
Categories
JavaScript React

How To Use React Router With React

If you want to make a single-page app with React, you need to add routing to your app. React Router is the most popular routing library for React apps. To use it, you have to add the library via npm.

In this piece, I will create an app with React and React Router in the simplest way possible.

To create a new React app, use the create-react-app code generator made by the developers of React. Here are the README and full documentation.

The app that will be created is an app that displays data from the Dog API.

To create the app, run npx create-react-app and follow the instructions, you will get a new app. Then, you are ready to install React Router.

To install it, run npm i react-router-dom. After that, install @material-ui/core and axios by running npm i @material-ui/core axios. Material UI provides a Material Design look to our app and Axios is an HTTP client which works at client-side apps.

In index.js, we have:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { breedsReducer, imagesReducer } from './reducers';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import { combineReducers } from 'redux'
const dogApp = combineReducers({
  breeds: breedsReducer,
  images: imagesReducer
})

const store = createStore(dogApp)ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
, document.getElementById('root')
);serviceWorker.unregister();

First, we add reducers to store state in a centrally available location. We make a file called reducer.js:

import { SET_BREEDS, SET_IMAGES } from './actions';

function breedsReducer(state = {}, action) {
 switch (action.type) {
    case SET_BREEDS:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state
   }
}

function imagesReducer(state = [], action) {
 switch (action.type) {
    case SET_IMAGES:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state
  }
}

export { breedsReducer, imagesReducer };

In actions.js, we add these constants for our Redux actions:

const SET_BREEDS = 'SET_BREEDS';
const SET_IMAGES = 'SET_IMAGES';export { SET_BREEDS, SET_IMAGES };

In actionCreators.js, we add:

import { SET_BREEDS, SET_IMAGES } from './actions';

const setBreeds = (breeds) => {
  return {
    type: SET_BREEDS,
    payload: breeds
  }
};

const setImages = (images) => {
  return {
    type: SET_IMAGES,
    payload: images
  }
};

export { setBreeds, setImages };

In app.js, we change the default code to:

import React, { useState, useEffect } from "react";
import './App.css';
import { setBreeds } from './actionCreators';
import { connect } from 'react-redux';
import { Router, Route, Link } from "react-router-dom";
import BreedsPage from './BreedsPage';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import { makeStyles } from '@material-ui/core/styles';
import { createBrowserHistory as createHistory } from 'history'
const history = createHistory();
const axios = require('axios');
const useStyles = makeStyles(theme => ({
  root: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  title: {
    flexGrow: 1,
  },
}));

function App({ setBreeds }) {
  const classes = useStyles();
  const [initialized, setInitialized] = useState(false);
  const [state, setState] = useState({
    openDrawer: false }); const titles = {
     '/': 'Dog App',
     '/breeds': 'Get Images By Breed - Dog App',
     '/subbreeds': 'Get Images By Breed or Sub-Breed - Dog App'
   }

   history.listen((location, action) => {
     document.title = titles[location.pathname];
     setState({ openDrawer: false });
   }); 

   const toggleDrawer = (open) => event => {
   if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
      return;
    }
    setState({ openDrawer: open });
   }; 

   const links = {
    'Home': '/',
    'Breeds': '/breeds',
    'Sub-Breeds': '/subbreeds',
   } const getBreeds = () => {
    setInitialized(true);
    axios.get('https://dog.ceo/api/breeds/list/all')
    .then((response) => {
      setBreeds(response.data.message);
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
    });
   } 

   useEffect(() => {
     if (!initialized) {
       getBreeds();
     }
   }); 

   return (
      <div className="App">
        <Router history={history}>
          <Drawer anchor="left" open={state.openDrawer} onClose=       {toggleDrawer(false)}>
            <List>
               <ListItem button>
                 <h2><b>Dog App</b></h2>
               </ListItem>
               {Object.keys(links).map((text) => (
                  <ListItem button key={text}>
                    <Link to={links[text]}>
                      <ListItemText primary={text} />
                   </Link>
                </ListItem>
                ))}
            </List>
          </Drawer>
           <AppBar position="static">
             <Toolbar>
               <IconButton edge="start" className={classes.menuButton} color="inherit" aria-label="Menu" onClick={toggleDrawer(true)}>
                 <i className="material-icons">menu</i>
               </IconButton>
               <Typography variant="h6" className={classes.title}>Dog App</Typography>
             </Toolbar>
          </AppBar>
          <Route path="/breeds/" component={BreedsPage} />
        </Router>
       </div>
      );
}

const mapStateToProps = (state) => ({
  breeds: state.breeds
})

const mapDispatchToProps = (dispatch) => ({
  setBreeds: breeds => dispatch(setBreeds(breeds))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

The <Route path=”/breeds/” component={BreedsPage} /> is where the route is defined. It must be inside <Router history={history}></Router>.

This is the routing part of our application. With this, we can go to the page when we type in http://localhost: 3000/breeds.

This block sets the title and hides the app drawer on the left when the route changes:

history.listen((location, action) => {
   document.title = titles[location.pathname];
   setState({ openDrawer: false });
});

We now create the pages for our apps which will use React Router for routing. First, we create a page for displaying breeds, we call it BreedPage.js.

The code will look like this:

import React from 'react';
import './BreedsPage.css';
import { setImages } from './actionCreators';
import { connect } from 'react-redux';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
import { makeStyles } from '@material-ui/core/styles';
import ImagesBox from './ImagesBox';
const axios = require('axios');
const useStyles = makeStyles(theme => ({
  formControl: {
    margin: theme.spacing(1),
    width: '90vw',
  },}));function BreedsPage({ breeds, setImages }) {
  const classes = useStyles();
  const [state, setState] = React.useState({
    breed: '',
  });
  const [initialized, setInitialized] = React.useState(false);
  const handleChange = name => event => {
    setState({...state, [name]: event.target.value,});
    if (name == 'breed') {
      getImagesByBreed(event.target.value);
    }
  }; 

  const getImagesByBreed = (breed) => {
    axios.get(`https://dog.ceo/api/breed/${breed}/images`)
    .then((response) => {
      setImages(response.data.message);
    })
    .catch((error) => {
      console.log(error);
    })
    .finally(() => {
    });
  } 

  React.useEffect(() => {
    if (!initialized) {
      setInitialized(true);
      setImages([]);
    }
  }); 

  return (
    <div className="App">
      <h1>Get Images By Breed</h1>
      <form>
       <FormControl className={classes.formControl}>
         <InputLabel>Breed</InputLabel>
         <Select
           value={state.breed}
           onChange={handleChange('breed')}
         >
           {Object.keys(breeds || {}).map(b =>
             <MenuItem value={b} key={b}>{b}</MenuItem>
           )}
         </Select>
       </FormControl>
       <ImagesBox></ImagesBox>
      </form>
    </div>
  );
}

const mapStateToProps = state => {
 return {
    breeds: state.breeds,
    images: state.images
  }
}

const mapDispatchToProps = dispatch => ({
  setImages: images => dispatch(setImages(images))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(BreedsPage);