Categories
React Ionic

Mobile Development with Ionic and React — Lifecycles and Routes

If we know how to create React web apps but want to develop mobile apps, we can use the Ionic framework.

In this article, we’ll look at how to get started with mobile development with the Ionic framework with React.

Lifecycle Methods in Functional Components

Ionic for React comes with its own lifecycle hooks.

They include the useIonViewDidEnter, useIonViewDidLeave , useIonViewWillEnter , and useIonViewWillLeave hooks.

The useIonViewDidEnter hook is run when the ionViewDidEnter event is triggered.

It’s called every time the view is visible.

useIonViewDidLeave is called when the ionViewDidLeave event is triggered.

This event is triggered when the page is fully transitioned in.

Any logic that we might not normally do when the view is visible can go here.

The useIonViewWillEnter hook is called when ionViewWillEnter .

It’s called every time the component is navigated to.

The useIonViewWillLeave callback can be used to run cleanup code.

We can put them all in a component by writing:

pages/Tab1.tsx

import React from 'react';
import { IonButton, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonText, IonTitle, IonToolbar, useIonViewDidEnter, useIonViewDidLeave, useIonViewWillEnter, useIonViewWillLeave } from '@ionic/react';
import './Tab1.css';

const Tab1: React.FC = () => {
  useIonViewDidEnter(() => {
    console.log('ionViewDidEnter event fired');
  });

  useIonViewDidLeave(() => {
    console.log('ionViewDidLeave event fired');
  });

  useIonViewWillEnter(() => {
    console.log('ionViewWillEnter event fired');
  });

  useIonViewWillLeave(() => {
    console.log('ionViewWillLeave event fired');
  });

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Tab 1</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent fullscreen>
        <IonGrid>
          <IonRow>
            <IonText>hello world</IonText>
          </IonRow>
        </IonGrid>
      </IonContent>
    </IonPage>
  );
};

export default Tab1;

React Navigation

We can add navigation to an Ionic React app with route components.

It comes with its own router to resolve the routes.

For example, we have:

App.tsx

import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import {
  IonApp,
  IonIcon,
  IonLabel,
  IonRouterOutlet,
  IonTabBar,
  IonTabButton,
  IonTabs
} from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import { ellipse, square, triangle } from 'ionicons/icons';
import Tab1 from './pages/Tab1';
import Tab2 from './pages/Tab2';
import Tab3 from './pages/Tab3';

/* 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';

const App: React.FC = () => (
  <IonApp>
    <IonReactRouter>
      <IonTabs>
        <IonRouterOutlet>
          <Route path="/tab1" component={Tab1} exact={true} />
          <Route path="/tab2" component={Tab2} exact={true} />
          <Route path="/tab3" component={Tab3} />
          <Route path="/" render={() => <Redirect to="/tab1" />} exact={true} />
        </IonRouterOutlet>
        <IonTabBar slot="bottom">
          <IonTabButton tab="tab1" href="/tab1">
            <IonIcon icon={triangle} />
            <IonLabel>Tab 1</IonLabel>
          </IonTabButton>
          <IonTabButton tab="tab2" href="/tab2">
            <IonIcon icon={ellipse} />
            <IonLabel>Tab 2</IonLabel>
          </IonTabButton>
          <IonTabButton tab="tab3" href="/tab3">
            <IonIcon icon={square} />
            <IonLabel>Tab 3</IonLabel>
          </IonTabButton>
        </IonTabBar>
      </IonTabs>
    </IonReactRouter>
  </IonApp>
);

export default App;

We have the Route components in the IonRouterOutlet to add our route components.

path has the URLs, component has the component to show when we reach the route. exact set to true means we match the exact URL.

Then to add navigation buttons, we add the IonTabButton components with the href prop set to the URL for the paths.

Conclusion

We can add lifecycle hooks and routes with Ionic React.

Categories
React Ionic

Getting Started with Mobile Development with Ionic and React

If we know how to create React web apps but want to develop mobile apps, we can use the Ionic framework.

In this article, we’ll look at how to get started with mobile development with the Ionic framework with React.

Getting Started

We can get started by installing a few things.

First, we install the Ionic CLI globally by running:

npm install -g @ionic/cli native-run cordova-res

Next, we can create our Ionic app project by running:

ionic start ionic-app tabs --type=react --capacitor

tabs adds tabs to the app.

type set to react means we’ll create a React project

--capacitor means we add Capacitor so we can run and build a native app from our project files.

Then we run:

npm install @ionic/react-hooks @ionic/pwa-elements

in the ionic-app project folder to install the React hooks for our project.

Then to run the app in the browser, we run:

ionic serve

Running the App with Genymotion

To run our app with Genymotion and built a native app, we need to do more things.

First, we run:

ionic build

to create the assets folder.

Then we run:

npx cap add android
npx cap sync

to add the Android dependencies.

Then we need to install Android Studio and Genymotion.

After we install Android Studio, we install the Genymotion plugin for Android Studio.

Once we did that, we run:

ionic capacitor run android --livereload --external --address=0.0.0.0

to preview our app in Genymotion.

Now we should see the app reload live.

Creating a Camera App

We can create a camera app easily with Ionic.

To do this, we go to Tab1.tsx and write:

pages/Tab1.tsx

import React, { useEffect, useState } from 'react';
import { IonButton, IonCol, IonContent, IonGrid, IonHeader, IonImg, IonPage, IonRow, IonTitle, IonToolbar } from '@ionic/react';
import ExploreContainer from '../components/ExploreContainer';
import './Tab1.css';
import { useCamera } from '@ionic/react-hooks/camera';
import { CameraResultType, CameraSource } from "@capacitor/core";

interface Photo {
  filepath: string;
  webviewPath?: string;
}

function usePhotoGallery() {
  const { getPhoto } = useCamera();
  const [photos, setPhotos] = useState<Photo[]>([]);

  const takePhoto = async () => {
    const cameraPhoto = await getPhoto({
      resultType: CameraResultType.Uri,
      source: CameraSource.Camera,
      quality: 100
    });

  const fileName = new Date().getTime() + '.jpeg';
    const newPhotos = [{
      filepath: fileName,
      webviewPath: cameraPhoto.webPath
    }, ...photos];
    setPhotos(newPhotos)
  };

  return {
    photos,
    takePhoto
  };
}

const Tab1: React.FC = () => {
  const { photos, takePhoto } = usePhotoGallery();

  return (
    <IonPage>
      <IonHeader>
        <IonToolbar>
          <IonTitle>Tab 1</IonTitle>
        </IonToolbar>
      </IonHeader>
      <IonContent fullscreen>
        <IonGrid>
          <IonRow>
            <IonButton onClick={takePhoto}>take photo</IonButton>
          </IonRow>
          <IonRow>
            {photos.map((photo, index) => (
              <IonCol size="6" key={index}>
                <IonImg src={photo.webviewPath} />
              </IonCol>
            ))}
          </IonRow>
        </IonGrid>
      </IonContent>
    </IonPage>
  );
};

export default Tab1;

This is the code for the whole camera app.

We created the usePhotoGallery hook that uses the useCamera hook to call the getPhoto function to create the cameraPhoto object.

With it, the camera will show.

Then we add the newPhotos array to get the photo and put it in the photos array.

The webviewPath has the path of the photo.

In the Tab1 component, we added an IonButton to show the take photo button.

We set the onClick prop to the takePhoto function to show the camera and take the photo.

Then once we’re done taking the photo, we get the photos from the photos array and display them.

Conclusion

We can create a simple app with Ionic easily.

Categories
React

React Testing — Getting Started

Automated tests are important for most apps.

In this article, we’ll take a look at how to write tests for React components.

Testing Tools

Create React App has test files and scripts built into the project.

First Test

We can write our first test by adding test files in our Create React App project.

If we have App.js :

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

In App.test.js , we write:

import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  const { getByText } = render(<App />);
  const linkElement = getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

to add the test.

The render function renders the App component that we imported.

Then we call getByText with a regex to get the element we’re looking for.

Finally, we call toBeInTheDocument to check linkElement is there.

Data Fetching

We add setup and teardown code with React tests.

Also, we can mock any HTTP requests in our test code so that we can run our tests in isolation.

For example, if we have the following component:

Answer.js

import React, { useState, useEffect } from "react";

export default function Answer(props) {
  const [data, setData] = useState({});

  async function fetchData() {
    const response = await fetch("https://yesno.wtf/api");
    setData(await response.json());
  }

  useEffect(() => {
    fetchData(props.id);
  }, []);

  return (
    <div>
      <p>{data.answer}</p>
    </div>
  );
}

Then we have to mock the Fetch API.

To do that, we can write:

Answer.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Answer from "./Answer";

let container = null;
beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("renders answer data", async () => {
  const fakeData = {
    answer: "yes",
    forced: false,
    image: "https://yesno.wtf/assets/yes/6-304e564038051dab8a5aa43156cdc20d.gif"
  };
  jest.spyOn(global, "fetch").mockImplementation(() =>
    Promise.resolve({
      json: () => Promise.resolve(fakeData)
    })
  );

  await act(async () => {
    render(<Answer />, container);
  });

  expect(container.querySelector("p").textContent)
    .toBe(fakeData.answer);
  global.fetch.mockRestore();
});

In the code above, we have the container which we use to mount our component.

We have the beforeEach hook to create the container element to mount our component in.

And we have the afterEach hook to unmount our component and remove the container.

Then in our 'render answer data' test, we create the fakeData object with our mock response data.

Then we mock the fetch function in Answer.js by using the jest.spyOn method.

global is the global object, 'fetch' is the fetch function.

mockImplementation lets us mock fetch with an object with the json method that’s set to a function that returns a promise that resolves to the fake data.

Then we mount our Answer component with act and await it to let the promise resolve.

And then we check what’s in the container ‘s p element to check if it has what we expect.

Then finally, we call global.fetch.mockRestore() to clear the mocks.

Conclusion

We can add tests and mock any data fetching code with Jest and React’s test library.

Categories
React

React Testing — Timers, and Snapshots

Automated tests are important for most apps.

In this article, we’ll take a look at how to write tests for React components.

Timers

We can mock timers in our React component tests.

For example, if we have:

import React, { useEffect } from "react";

export default function Card({ onSelect }) {
  useEffect(() => {
    const timeoutID = setTimeout(() => {
      onSelect(null);
    }, 5000);
    return () => {
      clearTimeout(timeoutID);
    };
  }, [onSelect]);

  return (
    <>
      <button
        data-testid='button'
        onClick={() => onSelect(1)}
      >
        button
    </button>
    </>
  );
}

Then we can test it by writing the following:

Card.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Card from "./card";

jest.useFakeTimers();

let container = null;
beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should select null after timing out", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });

  act(() => {
    jest.advanceTimersByTime(100);
  });
  expect(onSelect).not.toHaveBeenCalled();

  act(() => {
    jest.advanceTimersByTime(5000);
  });
  expect(onSelect).toHaveBeenCalledWith(null);
});

it("should clean up on being removed", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });
  act(() => {
    render(null, container);
  });
  act(() => {
    jest.advanceTimersByTime(5000);
  });
  expect(onSelect).not.toHaveBeenCalled();
});

it("should accept selections", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });
  act(() => {
    container
      .querySelector("[data-testid='button']")
      .dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });
  expect(onSelect).toHaveBeenCalledWith(1);
});

We have the usual beforeEach and afterEach hooks to create and remove the container for mounting our component for testing.

In the ‘should select null after timing out’, we set the timer to the time we want by calling the jest.advanceTimerByTime method.

Then we check what the mockedonSelect function is called with toHaveBeenCalledWith .

And we check if onSelect is called with toHaveBeenCalled .

In the “should clean up on being removed” test, we make sure that onSelect isn’t called after it’s unmounted.

In the “should accept selections” test, we triggered the click event on the button.

Then we check is onSelect is called with the argument we expect.

Snapshot Testing

We can test snapshots, which is the rendered component at a given time.

For example, if we have:

import React from "react";

export default function Hello({ name }) {
  return (
    <>
      <p>hello {name}</p>
    </>
  );
}

Then we can add the following test to test the component:

Hello.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import pretty from "pretty";
import Hello from "./hello";

let container = null;
beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should render a greeting", () => {
  act(() => {
    render(<Hello />, container);
  });
  expect(pretty(container.innerHTML)).toMatchInlineSnapshot(`"<p>hello </p>"`);

  act(() => {
    render(<Hello name="james" />, container);
  });
  expect(pretty(container.innerHTML)).toMatchInlineSnapshot(
    `"<p>hello james</p>"`
  );
});

We have the same beforeEach and afterEach hooks as before.

To test the component, we get the rendered HTML with container.innerHTML .

Then we check it against the HTML code with toMatchInlineSnapshot .

We have to install pretty and prettier by running:

npm i pretty prettier --save-dev

to get the pretty function to render the HTML.

Conclusion

We can mock timers and test React components with rendered HTML.

Categories
React

React Testing — Mocking Modules and Dispatching Events

Automated tests are important for most apps.

In this article, we’ll take a look at how to write tests for React components.

Mocking Modules

We can mock modules that don’t work well in a test environment.

For example, if we have the following components:

Map.js

import React from "react";

import { LoadScript, GoogleMap } from "react-google-maps";
export default function Map(props) {
  return (
    <LoadScript id="script-loader" googleMapsApiKey="YOUR_API_KEY">
      <GoogleMap id="example-map" center={props.center} />
    </LoadScript>
  );
}

Contact.js

import Map from "./map";

export default function Contact({ name, email }) {
  return (
    <div>
      <address>
        {name} {email}
      </address>
      <Map center={props.center} />
    </div>
  );
}

Then we can test the Contact component with a mocked Map component by writing:

Contact.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Contact from "./contact";

jest.mock("./map", () => {
  return function DummyMap(props) {
    return (
      <div data-testid="map">
        {props.center.lat}:{props.center.long}
      </div>
    );
  };
});

let container = null;
beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should render contact information", () => {
  const center = { lat: 0, long: 0 };
  act(() => {
    render(
      <Contact
        name="james"
        email="test@example.com"
        center={center}
      />,
      container
    );
  });

  expect(
    container.querySelector("address").textContent
  )
    .toContain("james test@example.com");

  expect(container.querySelector('[data-testid="map"]').textContent).toEqual(
    "0:0"
  );
});

We have:

jest.mock("./map", () => {
  return function DummyMap(props) {
    return (
      <div data-testid="map">
        {props.center.lat}:{props.center.long}
      </div>
    );
  };
});

to mock the Map component.

Then when we call render , we render with DummyMap instead of the actual Map component.

Events

To test events, we can dispatch real DOM events on DOM elements.

For instance, if we want to test the Toggle component:

import React, { useState } from "react";

export default function Toggle(props) {
  const [state, setState] = useState(false);
  return (
    <button
      onClick={() => {
        setState(previousState => !previousState);
        props.onChange(!state);
      }}
      data-testid="toggle"
    >
      {state ? "off" : "on"}
    </button>
  );
}

Then we can add a test file for it by writing:

Toggle.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Toggle from "./toggle";

let container = null;
beforeEach(() => {
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("changes value when clicked", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });
  const button = document.querySelector("[data-testid=toggle]");
  expect(button.innerHTML).toBe("on");
  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });
  expect(onChange).toHaveBeenCalledTimes(1);
  expect(button.innerHTML).toBe("off");
  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });
  expect(onChange).toHaveBeenCalledTimes(2);
  expect(button.innerHTML).toBe("on");
});

We mock the onChange method that we pass in as the value of the onChange prop.

Then we get the button with the selector[data-testid=toggle] from the Toggle component.

Then we can get the content of the button and how many times onChange has been called after we call dispatchEvent to dispatch a click MouseEvent .

We need to pass in { bubbles: true } so that React will delegate the event to the document.

Conclusion

We can mock modules that we can’t use conveniently in our tests.

Also, we can trigger events on elements and check the result after that.