Categories
JavaScript React

State and Lifecycle of React Components

React is a library for creating front end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.

In this article, we’ll look at the lifecycle of React components and how to change their internal state.

Changing Internal State

Props are immutable since we want to keep React components pure with respect to props. This means that when we pass in the same props, we always get the same output assuming nothing else changes.

This isn’t very useful in most cases since we want to change things inside a component.

To do this, we can manipulate the internal state of a component.

All class-based React components have internal state accessible via this.state, and we can change it by calling this.setState.

For example, if we want to create a Clock component that shows the current time and updates it every second, we can do that as follows:

class Clock extends React.Component {  
  constructor(props) {  
    super(props);  
    this.state = { date: new Date(), timer: undefined };  
  } 

  componentWillMount() {  
    this.updateDate();  
  } 

  componentWillUnmount() {  
    clearInterval(this.state.timer);  
  } 

  updateDate() {  
    const timer = setInterval(() => {  
      this.setState({ date: new Date() });  
    }, 1000);  
    this.setState({ timer });  
  } 

  render() {  
    return (  
      <div>  
        <p>It is {this.state.date.toLocaleTimeString()}.</p>  
      </div>  
    );  
  }  
}

In the code above, we declared a class Clock that extends React.Component. This means the code is a React component, which can have its own lifecycle methods and the render method.

Inside the Clock component, we declared the state field in the constructor which has the initial state date, and a timer state to hold the timer object that’s returned by setInterval.

Also, we called super(props) since Clock extends React.Component, and the React.Constructor takes props as an argument.

Then we have our componentDidMount lifecycle hook to load the initialization code when the Clock component is mounted into the DOM.

The updateDate method is called in componentDidMount to continuously update the this.state.date every second.

To update the date inside the setInterval callback, we called this.setState with an object inside.

The object has the state’s name as the key as we defined it in the first line, and the value is the new value that we want to set the state to.

We also called setState to set the timer field so that we can call clearInterval with it inside the componentWillUnmount. The componentWillUnmount hook is called when the component is removed from the DOM.

It’s a good place to run any code that’s used to clean things up.

Finally, in the render method, we render the date. This method is called whenever this.state changes by calling this.setState, so if we put this.state properties in there, the latest value of it will always be shown.

This means that this.state.date will be the latest date after:

this.setState({ date: new Date() });

is called.

componentDidMount and componentWillUnmount are lifecycle methods. They’re called only in specific parts of a React component’s lifecycle.

this.setState is asynchronous, so we shouldn’t assume that code that’s listed after a this.setstate call will run immediately after this.setState.

To mount it in the DOM, we call the ReactDOM.render method with Clock passed in as follows:

import React from "react";  
import ReactDOM from "react-dom";

class Clock extends React.Component {  
  constructor(props) {  
    super(props);  
    this.state = { date: new Date(), timer: undefined };  
  } 

  componentWillMount() {  
    this.updateDate();  
  } 

  componentWillUnmount() {  
    clearInterval(this.state.timer);  
  } 

  updateDate() {  
    const timer = setInterval(() => {  
      this.setState({ date: new Date() });  
    }, 1000);  
    this.setState({ timer });  
  } 

  render() {  
    return (  
      <div>  
        <p>It is {this.state.date.toLocaleTimeString()}.</p>  
      </div>  
    );  
  }  
}

const rootElement = document.getElementById("root");  
ReactDOM.render(<Clock />, rootElement);

Using State Correctly

We should never modify the state directly because it won’t trigger a re-rendering of a component.

So we shouldn’t write:

this.state.foo = 'bar';

Instead, we should always use this.setState as follows:

this.setState({foo: 'bar'});

State Updates May Be Asynchronous

State updates are asynchronous, so we shouldn’t depend on it run one after the other.

Also, we can’t just combine props and state values within setState .

For example, the following may fail:

this.setState({  
  count: this.state.count + this.props.increment,  
});

Instead, we write:

this.setState((state, props) => ({  
  count: state.count + props.increment  
}));

to get the latest values from props to update the code.

Regular and arrow functions both work, so we can also write:

this.setState(function(state, props) {  
  return {  
    count: state.count + props.increment  
  };  
});

State Updates are Merged

When we call setState, React merges the object that we pass into setState into the current state.

For example, if we have 2 states, firstName and lastName, then when we called this.setstate({ lastName });, this.state.firstName stays intact, while this.state.lastName is updated with the latest value of lastName.

Data Flows Down

States are always inside a component, and it can be passed down to child components and elements as props.

React apps has a unidirectional data flow that goes from parent to child.

Conclusion

We can create a React component that is dynamic by storing states inside a component.

The state object is stored in the state field of the React component class.

We can set the state by running this.setState.

React components have lifecycles that trigger methods in the React component class. These are called lifecycle hooks.

We can use them to run code at certain stages of a React component’s lifecycle.

Categories
JavaScript React

Add Captchas to a React App with reaptcha

Captchas stands for ‘Completely Automated Public Turing test to tell Computers and Humans Apart’. It’s often used to prevent automated traffic from tampering with websites.

With React, we can easily add a captcha to our app by using the reaptcha package. It’s very easy to use.

Installation

We can install it by running:

npm install --save reaptcha

We can also use yarn to install it y running:

yarn add reaptcha

Basic Usage

We can use it by signing up for a Recaptcha API key so that we can use Google’s Recaptcha service to use component. It works by injecting the Recaptcha scripts into our React app.

To sign up for an API key pair, go to https://www.google.com/recaptcha/ to sign up for an API key for free.

When we sign up for a key, we should sign up for a V2 Recaptcha key and add the domain that we want our Recaptcha widget to work on by entering the domain without any ports or child paths.

Once we signed up for an API key pair, we click Copy Site Key to copy the key that’s used on client side.

Next, we create our React app project and then write the following code:

import React from "react";
import Reaptcha from "reaptcha";

export default function App() {
  const [verified, setVerified] = React.useState(false);

  const onVerify = e => {
    setVerified(true);
  };

  return (
    <div className="App">
      <form>
        <Reaptcha
          sitekey="your_key"
          onVerify={onVerify}
        />
        <button type="submit" disabled={!verified}>
          Submit
        </button>
      </form>
    </div>
  );
}

In the code above, we put in our key as the value of siteKey. Then we have the onVerify callback, which is called when the captcha displayed is verified.

The onVerifed function is the callback that’s called when the captcha is successfully verified. Therefore, we use call setVerified there to enable the Submit button.

If we get an error, check if our domain is entered corrected in the API key settings. Also, we must be using the V2 version of reCAPTCHA with this package.

Once we did that, we should see a captcha, which enables the Submit button when we verified the displayed captcha.

Loading Captcha Manually

Reaptcha can also load captchas only when the user explicity does something to make it load.

We have to call the captcha’s renderExplicitly method to load the captcha. For instance, we can do that as follows:

import React from "react";
import Reaptcha from "reaptcha";

export default function App() {
  const [verified, setVerified] = React.useState(false);
  const [captchaReady, setCaptchaReady] = React.useState(false);
  const captcha = React.createRef();
  const onVerify = e => {
    setVerified(true);
  };

  const onLoad = () => {
    setCaptchaReady(true);
  };

  return (
    <div className="App">
      <button
        disabled={!captchaReady}
        onClick={() => {
          captcha.current.renderExplicitly();
        }}
      >
        Render reCAPTCHA
      </button>
      <form>
        <Reaptcha
          onLoad={onLoad}
          sitekey="your_key"
          onVerify={onVerify}
          ref={captcha}
          explicit
        />
        <button type="submit" disabled={!verified}>
          Submit
        </button>
      </form>
    </div>
  );
}

In the code above, we have a button to load the captcha when it’s clicked. It’s only enabled with then captcha script is finished loading so we can call the renderExplicitly method to load it.

In the Reaptcha component, we added the onLoad prop that calls the onLoad method. which calls setCaptchaReady to set the captchaReady property totrue` to enable the button.

Once is captcha is ready and the user clicks the Render reCAPTCHA button, we call captcha.current.renderExplicitly(); where captcha is the ref that we referenced in Reaptcha.

Most importantly, we added the explicit prop so that the captcha has to be loaded with an explicit user action.

Invisible Captchas

Reaptcha also supports invisible captchas. We can set the size prop to be invisible to make an invisible captcha.

For instance, we can create an invisible captcha as follows:

import React from "react";
import Reaptcha from "reaptcha";

export default function App() {
  const [verified, setVerified] = React.useState(false);
  const captcha = React.createRef();
  const onVerify = e => {
    setVerified(true);
  };

  return (
    <div className="App">
      <button
        onClick={() => {
          captcha.current.execute();
        }}
      >
        Execute reCAPTCHA
      </button>
      <form>
        <Reaptcha
          sitekey="your_key"
          onVerify={onVerify}
          ref={captcha}
          size="invisible"
        />
        <button type="submit" disabled={!verified}>
          Submit
        </button>
      </form>
    </div>
  );
}

We have to create a key for the V2 Invisible Captcha so that we can incorporate invisible captchas into our app.

Then we call captcha.current.execute(); where captcha is the ref for the Reaptcha component.

Methods

We can call the reset method on the Reaptcha ref to reset the reCAPTCHA instance and getResponse methos to return the response from reCAPTCHA. They both return promises.

Other Options

Other options that we can pass into the Reaptcha component as props include:

  • tabindex – HTML tab index
  • inject – boolean to indicate whether we want to inject the captcha script to the DOM automatically.
  • isolated – boolean to indicate that this component shouldn’t interfere with existing reCAPTCHA installations on a page
  • onError – error handler
  • children – a function to access instance methods without the need to use refs.

Conclusion

Reaptcha is an easy to use React component for incorporating the V2 reCAPTCHA script into our app. All we have to do is to add the component, sign up for the reCAPTCHA API key and then set a few options to get the captcha added to our React app.

Categories
JavaScript React

Add a PDF Reader to a React App with react-pdf-viewer

Adding React viewers is a common requirement for web apps. With React, there’s the react-pdf-viewer package that lets us add PDF viewers to React apps easily.

In this article, we’ll look at how to use it to add a PDF viewer to our React app.

Installation

We can install it by running:

npm install @phuocng/react-pdf-viewer

Basic Usage

After installing it, we can use it as follows:

import React from "react";
import Viewer, { Worker } from "@phuocng/react-pdf-viewer";

import "@phuocng/react-pdf-viewer/cjs/react-pdf-viewer.css";

export default function App() {
  return (
    <div className="App">
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@2.2.228/build/pdf.worker.min.js">
        <div style={{ height: "750px" }}>
          <Viewer fileUrl="dummy.pdf" />
        </div>
      </Worker>
    </div>
  );
}

In the code above, we included the CSS file that comes with the package, and the Viewer for opening the PDF viewer and the Worker component for loading the PDF specified in the fileUrl prop as in the background.

We have to include the workerUrl prop with that URL so that the worker in that location is run.

Then we’ll get a PDF viewer that has zoom in and out controls, page navigation, document properties, and download options.

In a Create React App project, static files like PDFs should be in the public folder so that it can be loaded. Also, PDFs have to be in the same domain as the React app so that we won’t get CORS errors.

Options

There’re options for customization. We can change the layout of the sidebar, toolbar, and replace default controls with React components of our choice.

Layout options available include:

  • isSidebarOpened – boolean to indicate whether we want the sidebar to open or not
  • main – the main part of the viewer (a Slot component object)
  • toolbar – toolbar part (a RenderToolbar component object)
  • sidebarSlot object to define the sidebar of the viewer

For instance, we can write the following code to add a sidebar to display pages and a layout component do define a layout for our PDF viewer as follows:

import React from "react";
import Viewer, {
  Worker,
} from "@phuocng/react-pdf-viewer";

import "@phuocng/react-pdf-viewer/cjs/react-pdf-viewer.css";


export default function App() {
  const renderToolbar = (toolbarSlot) => {
    return (
      <div
        style={{
          alignItems: 'center',
          display: 'flex',
          width: '100%',
        }}
      >
        <div
          style={{
            alignItems: 'center',
            display: 'flex',
          }}
        >
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.searchPopover}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.previousPageButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.currentPageInput} / {toolbarSlot.numPages}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.nextPageButton}
          </div>
        </div>
        <div
          style={{
            alignItems: 'center',
            display: 'flex',
            flexGrow: 1,
            flexShrink: 1,
            justifyContent: 'center',
          }}
        >
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.zoomOutButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.zoomPopover}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.zoomInButton}
          </div>
        </div>
        <div
          style={{
            alignItems: 'center',
            display: 'flex',
            marginLeft: 'auto',
          }}
        >
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.fullScreenButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.openFileButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.downloadButton}
          </div>
          <div style={{ padding: '0 5px' }}>
            {toolbarSlot.moreActionsPopover}
          </div>
        </div>
      </div>
    );
  };

  const layout = (
    isSidebarOpened,
    main,
    toolbar,
    sidebar
  ) => {
    return (
      <div
        style={{
          border: '1px solid rgba(0, 0, 0, .3)',
          display: 'grid',
          gridTemplateAreas: "'toolbar toolbar' 'sidebar main'",
          gridTemplateColumns: '30% 1fr',
          gridTemplateRows: '40px calc(100% - 40px)',
          height: '100%',
          overflow: 'hidden',
          width: '100%',
        }}
      >
        <div
          style={{
            alignItems: 'center',
            backgroundColor: '#EEE',
            borderBottom: '1px solid rgba(0, 0, 0, .1)',
            display: 'flex',
            gridArea: 'toolbar',
            justifyContent: 'center',
            padding: '4px',
          }}
        >
          {toolbar(renderToolbar)}
        </div>
        <div
          style={{
            borderRight: '1px solid rgba(0, 0, 0, 0.2)',
            display: 'flex',
            gridArea: 'sidebar',
            justifyContent: 'center',
          }}
        >
          {sidebar.children}
        </div>
        <div
          {...main.attrs}
          style={Object.assign({}, {
            gridArea: 'main',
            overflow: 'scroll',
          }, main.attrs.style)}
        >
          {main.children}
        </div>
      </div>
    );
  };

  return (
    <Worker workerUrl="https://unpkg.com/pdfjs-dist@2.2.228/build/pdf.worker.min.js">
      <Viewer
        fileUrl='dummy.pdf'
        layout={layout}
      />
    </Worker>
  );
}

The default layout is the following:

┌───────────┬───────────┐
│ toolbar   │ toolbar   │
├───────────┼───────────┤
│ sidebar   │ main      │
└───────────┴───────────┘

In the code above, we reference those parts in the places we wish to place in the layout component. The children attribute has the parts of each component. attrs has the default props of each component, which we can change.

The renderToolbar function is a higher-order component that takes a toolbarSlot prop, which has the parts of the toolbar and we can place them accordingly according to our needs. In the example above, we put them in different divs and added our own styles to each div.

The grid above is a CSS grid, so we can modify the layout as we please and it’ll will in all modern browsers.

Those pats are passed in as props in the layout component. So we can reference them directly from the parameters of layout.

Conclusion

The react-pdf-viewer package is a very useful PDF viewer that’s designed with both performance and usability in mind. The default layout and controls are already very good. Performance comes from loading PDFs in the background with a web worker.

It’s also very customizable, we can define a layout component that has toolbar, sidebar and main as props and then we can customize them as we wish.

Categories
JavaScript React

Use React to Display Images in a Grid Like Google and Flickr

If you use image search websites like Google Image Search or Flickr, you will notice that their images display in a grid that looks like a wall of bricks. The images are uneven in height, but equal in width. This is called the masonry effect because it looks like a wall of bricks.

To implement the masonry effect, we have to set the width of the image proportional to the screen width and set the image height to be proportional to the aspect ratio of the image.

This is a pain to do if it’s done without any libraries, so people have made libraries to create this effect.

In this article, we will build a photo app that allows users to search for images and display images in a masonry grid. The image grid will have infinite scroll to get more images. We will build it with React and the React Masonry Component library. For infinite scrolling, we will use the React Infinite Scroller library. We will wrap the React Infinite Scroller outside the React Masonry Component to get infinite scrolling with the masonry effect when displaying images.

Our app will display images from the Pixabay API. You can view the API documentation and register for a key at https://pixabay.com/api/docs/

To start, we run Create React App to create the app. Run npx create-react-app photo-app to create the initial code for the app.

Then we install our own libraries. We need React Infinite Scroller, React Masonry Component, Bootstrap for styling, Axios for making HTTP requests, Formik and Yup for form value data binding and form validation, and React Router for routing URLs to our pages.

To install all the packages, run:

npm i axios bootstrap formik react-bootstrap react-infinite-scroller react-masonry-component react-router-dom yup

to install all the packages.

With all the packages installed, we can start building the app. First start with replacing the code in App.js with:

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

function App() {  
  return (  
    <div className="App">  
      <Router history={history}>  
        <TopBar />  
        <Route path="/" exact component={HomePage} />  
        <Route path="/imagesearch" exact component={ImageSearchPage} />  
      </Router>  
    </div>  
  );  
}

export default App;

to add the top bar and the routes for our app into the entry point of the app.

Next remove all the code in App.css and add:

.page {  
  padding: 20px;  
}

to add padding to our pages.

Then we set our React Masonry Component options by creating a exports.js in the src folder and add:

export const masonryOptions = {  
  fitWidth: true,  
  columnWidth: 300,  
  gutter: 5  
};

These options are very important. We need to set fitWidth to true to center our grid. columnWidth must be a number to get constant width. It will scale according to screen size only with constant width. The gutter value is the margin between items.

The full list of options are at https://masonry.desandro.com/options.html

Next we create our app’s home page by creating HomePage.js in the src folder and add:

import React from "react";  
import { getImages } from "./request";  
import InfiniteScroll from "react-infinite-scroller";  
import Masonry from "react-masonry-component";  
import "./HomePage.css";  
import { masonryOptions } from "./exports";

function HomePage() {  
  const [images, setImages] = React.useState([]);  
  const [page, setPage] = React.useState(1);  
  const [total, setTotal] = React.useState(0);  
  const [initialized, setInitialized] = React.useState(false); 
  const getAllImages = async (pg = 1) => {  
    const response = await getImages(page);  
    let imgs = images.concat(response.data.hits);  
    setImages(imgs);  
    setTotal(response.data.total);  
    pg++;  
    setPage(pg);  
  }; 

  React.useEffect(() => {  
    if (!initialized) {  
      getAllImages();  
      setInitialized(true);  
    }  
  }); 

  return (  
    <div className="page">  
      <h1 className="text-center">Home</h1>  
      <InfiniteScroll  
        pageStart={1}  
        loadMore={getAllImages}  
        hasMore={total > images.length}  
      >  
        <Masonry  
          className={"grid"}  
          elementType={"div"}  
          options={masonryOptions}  
          disableImagesLoaded={false}  
          updateOnEachImageLoad={false}  
        >  
          {images.map((img, i) => {  
            return (  
              <div key={i}>  
                <img src={img.previewURL} style={{ width: 300 }} />  
              </div>  
            );  
          })}  
        </Masonry>  
      </InfiniteScroll>  
    </div>  
  );  
}  
export default HomePage;

In the home page, we just get the images when the page loads. When the user scroll down, we load more images by adding 1 to the currentpage value and get the image with the page number.

With the InfiniteScroll component, which is provided by React Infinite Scroll, wrapped outside the Masonry component, which is provided by React Masonry Component, we display our images in a grid, and also display more when the user scrolls down until the length of the images array is greater than or equal to the total, which is from the total field given by the Pixabay API’s results.

We load images when the page loads by checking if initialized flag is true, we only load images on page load if initialized is false and the when the request is first made to the API and succeeds, then we set initialized flag to true to stop requests from being made on every render.

Next we create a image search page by creating the ImageSearchPage.js file and adding the following:

import React from "react";  
import { Formik } from "formik";  
import Form from "react-bootstrap/Form";  
import Col from "react-bootstrap/Col";  
import Button from "react-bootstrap/Button";  
import * as yup from "yup";  
import InfiniteScroll from "react-infinite-scroller";  
import Masonry from "react-masonry-component";  
import { masonryOptions } from "./exports";  
import { searchImages } from "./request";

const schema = yup.object({  
  keyword: yup.string().required("Keyword is required")  
});

function ImageSearchPage() {  
  const [images, setImages] = React.useState([]);  
  const [keyword, setKeyword] = React.useState([]);  
  const [page, setPage] = React.useState(1);  
  const [total, setTotal] = React.useState(0);  
  const [searching, setSearching] = React.useState(false); 
  
  const handleSubmit = async evt => {  
    const isValid = await schema.validate(evt);  
    if (!isValid) {  
      return;  
    }  
    setKeyword(evt.keyword);  
    searchAllImages(evt.keyword, 1);  
  }; 

  const searchAllImages = async (keyword, pg = 1) => {  
    setSearching(true); 
    const response = await searchImages(keyword, page);  
    let imgs = response.data.hits;  
    setImages(imgs);  
    setTotal(response.data.total);  
    setPage(pg);  
  }; 

  const getMoreImages = async () => {  
    let pg = page;  
    pg++;  
    const response = await searchImages(keyword, pg);  
    const imgs = images.concat(response.data.hits);  
    setImages(imgs);  
    setTotal(response.data.total);  
    setPage(pg);  
  }; 

  React.useEffect(() => {}); 
  
  return (  
    <div className="page">  
      <h1 className="text-center">Search</h1>  
      <Formik validationSchema={schema} onSubmit={handleSubmit}>  
        {({  
          handleSubmit,  
          handleChange,  
          handleBlur,  
          values,  
          touched,  
          isInvalid,  
          errors  
        }) => (  
          <Form noValidate onSubmit={handleSubmit}>  
            <Form.Row>  
              <Form.Group as={Col} md="12" controlId="keyword">  
                <Form.Label>Keyword</Form.Label>  
                <Form.Control  
                  type="text"  
                  name="keyword"  
                  placeholder="Keyword"  
                  value={values.keyword || ""}  
                  onChange={handleChange}  
                  isInvalid={touched.keyword && errors.keyword}  
                />  
                <Form.Control.Feedback type="invalid">  
                  {errors.keyword}  
                </Form.Control.Feedback>  
              </Form.Group>  
            </Form.Row>  
            <Button type="submit" style={{ marginRight: "10px" }}>  
              Search  
            </Button>  
          </Form>  
        )}  
      </Formik>  
      <br />  
      <InfiniteScroll  
        pageStart={1}  
        loadMore={getMoreImages}  
        hasMore={searching && total > images.length}  
      >  
        <Masonry  
          className={"grid"}  
          elementType={"div"}  
          options={masonryOptions}  
          disableImagesLoaded={false}  
          updateOnEachImageLoad={false}  
        >  
          {images.map((img, i) => {  
            return (  
              <div key={i}>  
                <img src={img.previewURL} style={{ width: 300 }} />  
              </div>  
            );  
          })}  
        </Masonry>  
      </InfiniteScroll>  
    </div>  
  );  
}  
export default ImageSearchPage;

We do not load images on the first load on this page. Instead, the user enters a search term in the form and when the user clicks the Search button, then handleSubmit is called. The evt object has the form values, which is updated by the Formik component. Yup provides the form validation object with the schema object, we just check if keyword is required.

In the handlesubmit function, we get the evt object, which we validate against the schema by callingschema.validate, which returns a promise. If the promise returns to something truthy, then we proceed with making the request to the Pixabay API with the search keyword and page number.

We have the same setup as the home page for the infinite scroll and masonry effect image grid. The only difference is that we call the searchAllImages function which has similar logic as the getAllImages function, except that we pass in the keyword parameter in addition to the page parameter. We set the imgs variable to the array returned from the Pixabay API and set the images by calling setImages. We also set the page by calling setPage.

When the user scrolls far enough down that content runs out, the getMoreImages function is called when images.length is less than the total. The total is set by getting the total field from the API.

We use masonryOptions from exports.js just like in the home page and display images the same way.

Next create request.js in the src folder to add the code for making HTTP requests to the back end, like so:

const axios = require("axios");  
const APIURL = "https://pixabay.com/api";

export const getImages = (page = 1) =>  
  axios.get(`${APIURL}/?page=${page}&key=${process.env.REACT_APP_APIKEY}`);

export const searchImages = (keyword, page = 1) =>  
  axios.get(  
    `${APIURL}/?page=${page}&key=${process.env.REACT_APP_APIKEY}&q=${keyword}`  
  );

We have the getImages for just getting images and searchImages that also sends the search term to the API. process.env.REACT_APP_APIKEY is from setting the REACT_APP_APIKEY variable in the .env file in the project’s root folder.

Next create 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 { withRouter } from "react-router-dom";

function TopBar({ location }) {  
  React.useEffect(() => {}); 

  return (  
    <Navbar bg="primary" expand="lg" variant="dark">  
      <Navbar.Brand href="#home">Photo App</Navbar.Brand>  
      <Navbar.Toggle aria-controls="basic-navbar-nav" />  
      <Navbar.Collapse id="basic-navbar-nav">  
        <Nav className="mr-auto">  
          <Nav.Link href="/" active={location.pathname == "/"}>  
            Home  
          </Nav.Link>  
          <Nav.Link  
            href="/imagesearch"  
            active={location.pathname == "/imagesearch"}  
          >  
            Search  
          </Nav.Link>  
        </Nav>  
      </Navbar.Collapse>  
    </Navbar>  
  );  
}  

export default withRouter(TopBar);

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app. We check the location.pathname to highlight the right links by setting the active prop, where the location prop is provided by React Router by wrapping the withRouter function outside the TopBar component.

Finally, in index.js , we 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>Photo 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 add the Bootstrap CSS and change the title.

Categories
JavaScript React

How to Build Ionic Apps with React

With the latest version of the Ionic framework, a tool that lets you write mobile apps with web technologies, you can use React to build your mobile application. Before v4, Ionic only provided components for Angular, but now React has complete support for the components.

Ionic 4 is a mobile app framework and a component library. You can build mobile apps, progressive web apps, and normal web apps. The component library can also be used on its own. Of course, the hardware support remains in place for you to use if you wish.

The full reference for the React version of Ionic is at https://ionicframework.com/docs.

In this article, we will build a React web app with Ionic that does currency conversion. The home page will display the list of the latest exchange rates and another page will have a form to convert the currencies of your choice. To get the data, we use the Foreign exchange rates API located at https://exchangeratesapi.io/. We use the Open Exchange Rates API located at https://openexchangerates.org/ to get the list of currencies.

Getting Started

To start, we will install the Ionic CLI. We run:

npm install -g ionic@latest

Next, we run:

ionic start currency-converter --type=react

Then we select the Sidenav option to create an Ionic project with a left sidebar.

We will also need to install some other packages. We need Axios to make HTTP requests and MobX for state management. Run npm i axios mobx mobx-react in our project folder to install them.

Now we are ready to create some pages. In the pages folder, create ConvertCurrencyPage.jsx and add:

import {
  IonButtons,
  IonContent,
  IonHeader,
  IonItem,
  IonList,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonInput,
  IonLabel,
  IonSelect,
  IonSelectOption,
  IonButton
} from "@ionic/react";
import React from "react";
import { observer } from "mobx-react";
import { getExchangeRates } from "../requests";
const ConvertCurrencyPage = ({ currenciesStore }) => {
  const [fromCurrencies, setFromCurrencies] = React.useState({});
  const [toCurrencies, setToCurrencies] = React.useState({});
  const [values, setValues] = React.useState({ amount: 0 } as any);
  const [submitting, setSubmitting] = React.useState(false);
  const [toAmount, setToAmount] = React.useState(0);
const convertCurrency = async () => {
    setSubmitting(true);
    if (values.amount <= 0 || !values.from || !values.to) {
      return;
    }
    const { data } = await getExchangeRates(values.from);
    const rate = data.rates[values.to];
    setToAmount(values.amount * rate);
  };

  React.useEffect(() => {
    const fromCurrencies = {};
    for (let key in currenciesStore.currencies) {
      if (key != values.to) {
        fromCurrencies[key] = currenciesStore.currencies[key];
      }
    }
    setFromCurrencies(fromCurrencies);

  const toCurrencies = {};
    for (let key in currenciesStore.currencies) {
      if (key != values.from) {
        toCurrencies[key] = currenciesStore.currencies[key];
      }
    }
    setToCurrencies(toCurrencies);
  }, [currenciesStore.currencies, values.from, values.to]);

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonMenuButton />
          </IonButtons>
          <IonTitle>Convert Currency</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList lines="none">
          <IonItem>
            <IonInput
              type="number"
              value={values.amount}
              color={!values.amount && submitting ? "danger" : undefined}
              min="0"
              onIonChange={ev =>
                setValues({ ...values, amount: (ev.target as any).value })
              }
            ></IonInput>
          </IonItem>
          <IonItem>
            <IonLabel>Currency to Convert From</IonLabel>
            <IonSelect
              placeholder="Select One"
              color={!values.from && submitting ? "danger" : undefined}
              onIonChange={ev =>
                setValues({ ...values, from: (ev.target as any).value })
              }
            >
              {Object.keys(fromCurrencies).map(key => {
                return (
                  <IonSelectOption value={key} key={key}>
                    {(fromCurrencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
          <IonItem>
            <IonLabel>Currency to Convert To</IonLabel>
            <IonSelect
              placeholder="Select One"
              color={!values.to && submitting ? "danger" : undefined}
              onIonChange={ev =>
                setValues({ ...values, to: (ev.target as any).value })
              }
            >
              {Object.keys(toCurrencies).map(key => {
                return (
                  <IonSelectOption value={key} key={key}>
                    {(toCurrencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
          <IonItem>
            <IonButton size="default" fill="solid" onClick={convertCurrency}>
              Convert
            </IonButton>
          </IonItem>
          {toAmount ? (
            <IonItem>
              {values.amount} {values.from} is {toAmount} {values.to}.
            </IonItem>
          ) : (
            undefined
          )}
        </IonList>
      </IonContent>
    </IonPage>
  );
};
export default observer(ConvertCurrencyPage);

This adds a form to convert currency from one to another. In this form, we filter by choices by excluding from the currency being converted to from the first drop down and exclude the currency being converted from in the second dropdown. Also, we have an ion-input for the amount being converted. We get the values of the currency from the currenciesStore, which is the MobX store that gets the list of currencies from. In the IonSelect components we set the onIonChange props to the handler functions that set the drop down’s values. We also set the placeholder for all the inputs and selects. In the IonInput component, we do the same with the onIonChange handler. We show the correct value by using the variables set in the values object during change events.

When the user clicks Convert, we run the convertCurrency function. We check if the values are set correctly before running the rest of the code. If that succeeds, then we run the getExchangeRates function imported from requests.js, then we set the final toAmount by multiplying the rate with the amount.

The useEffect callback is used for excluding the currency to convert from the currency to convert to list and vice versa. The array in the second argument useEffect specifies which values to watch for.

The observer function in the last line is for designating the component to watch the latest values from the MobX stores.

Next in Home.tsx, we replace the existing code with:

import {
  IonButtons,
  IonContent,
  IonHeader,
  IonItem,
  IonLabel,
  IonList,
  IonListHeader,
  IonMenuButton,
  IonPage,
  IonTitle,
  IonToolbar,
  IonSelect,
  IonSelectOption
} from "@ionic/react";
import React from "react";
import "./Home.css";
import { getExchangeRates } from "../requests";
import { CurrencyStore } from "../stores";
import { observer } from "mobx-react";
const HomePage = ({ currencyStore, currenciesStore }) => {
  const [rates, setRates] = React.useState({});
  const getRates = async () => {
    const { data } = await getExchangeRates(
      (currencyStore as CurrencyStore).currency
    );
    setRates(data.rates);
  };

  React.useEffect(() => {
    getRates();
  }, [(currencyStore as CurrencyStore).currency]);
  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonButtons slot="start">
            <IonMenuButton />
          </IonButtons>
          <IonTitle>Home</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent>
        <IonList lines="none">
          <IonListHeader>Latest Exchange Rates</IonListHeader>
          <IonItem>
            <IonLabel>Currency</IonLabel>
            <IonSelect
              placeholder="Select One"
              onIonChange={ev => {
                (currencyStore as CurrencyStore).setCurrency(
                  ev.target && (ev.target as any).value
                );
              }}
            >
              {Object.keys(currenciesStore.currencies).map(key => {
                return (
                  <IonSelectOption
                    value={key}
                    key={key}
                    selected={
                      (currencyStore as CurrencyStore).currency
                        ? key == (currencyStore as CurrencyStore).currency
                        : key == "AUD"
                    }
                  >
                    {(currenciesStore.currencies as any)[key]}
                  </IonSelectOption>
                );
              })}
            </IonSelect>
          </IonItem>
        </IonList>
        <IonList lines="none">
          <IonListHeader>Exchange Rates</IonListHeader>
          {Object.keys(rates).map(key => {
            console.log(rates);
            return (
              <IonItem>
                {key}: {rates[key]}
              </IonItem>
            );
          })}
        </IonList>
      </IonContent>
    </IonPage>
  );
};
export default observer(HomePage);

In this file, we display the exchange rates from an API. We get the currencies from the currenciesStore so that users can see exchange rates based on different currencies. The items are displayed in an IonList provided by Ionic.

The observer function in the last line is for designating the component to watches the latest values from the MobX stores.

Next in App.tsx, replace the following code with:

import React from "react";
import { Redirect, Route } from "react-router-dom";
import { IonApp, IonRouterOutlet, IonSplitPane } from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { AppPage } from "./declarations";
import Menu from "./components/Menu";
import Home from "./pages/Home";
import { home, cash } from "ionicons/icons";
/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";
/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";
/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";
/* Theme variables */
import "./theme/variables.css";
import ConvertCurrencyPage from "./pages/ConvertCurrencyPage";
import { CurrencyStore, CurrenciesStore } from "./stores";
import { getCurrenciesList } from "./requests";
const currencyStore = new CurrencyStore();
const currenciesStore = new CurrenciesStore();
const appPages: AppPage[] = [
  {
    title: "Home",
    url: "/home",
    icon: home
  },
  {
    title: "Convert Currency",
    url: "/convertcurrency",
    icon: cash
  }
];
const App: React.FC = () => {
  const [initialized, setInitialized] = React.useState(false);
  const getCurrencies = async () => {
    const { data } = await getCurrenciesList();
    currenciesStore.setCurrencies(data);
    setInitialized(true);
  };
  React.useEffect(() => {
    if (!initialized) {
      getCurrencies();
    }
  });
  return (
    <IonApp>
      <IonReactRouter>
        <IonSplitPane contentId="main">
          <Menu appPages={appPages} />
          <IonRouterOutlet id="main">
            <Route
              path="/home"
              render={() => (
                <Home
                  currencyStore={currencyStore}
                  currenciesStore={currenciesStore}
                />
              )}
              exact={true}
            />
            <Route
              path="/convertcurrency"
              render={() => (
                <ConvertCurrencyPage
                  currenciesStore={currenciesStore}
                />
              )}
              exact={true}
            />
            <Route exact path="/" render={() => <Redirect to="/home" />} />
          </IonRouterOutlet>
        </IonSplitPane>
      </IonReactRouter>
    </IonApp>
  );
};
export default App;

We modified the routes so that we use the render prop instead of the component prop since we want to pass in our MobX stores into the components. The stores contain the currently selected currency for the home page and the list of currencies for both pages.

We get/set the list of currencies here for both components.

Next, create requests.ts in the src folder and add:

const axios = require('axios');
const APIURL = 'https://api.exchangeratesapi.io';
const OPEN_EXCHANGE_RATES_URL = 'http://openexchangerates.org/api/currencies.json';

export const getExchangeRates = (baseCurrency: string) => axios.get(`${APIURL}/latest?base=${baseCurrency}`)
export const getCurrenciesList = () => axios.get(OPEN_EXCHANGE_RATES_URL)

This the code for making the HTTP requests to get the currencies and exchange rates.

Next create store.ts and add:

import { observable, action } from "mobx";
class CurrencyStore {
    @observable currency: string = 'AUD';
    @action setCurrency(currency: string) {
        this.currency = currency;
    }
}
class CurrenciesStore {
    @observable currencies = {};
    @action setCurrencies(currencies) {
        this.currencies = currencies;
    }
}
export { CurrencyStore, CurrenciesStore };

The CurrencyStore is for storing the selected currency in the home page and to show the exchange rates based on the selected currency. currency is the value observed by the home page and setCurrency sets the currency value. Similarly, CurrenciesStore stores a list of currencies retrieved in App.tsx where setCurrencies is called.

Next in tsconfig.json, we replace the existing code with:

{
  "compilerOptions": {
      "experimentalDecorators": true,
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "noImplicitAny": false
  },
  "include": [
    "src"
  ]
}

This changes noImplicitAny and sets it to true.