web3 tutorial [06/10] - react: building the connect wallet screen

by parttimelarry

Creating a User Interface

So far we have spent all of our time on the backend of our dapp. We discussed Solidity data types and functions, created a smart contract, and deployed it to an Ethereum test network.

But how does anyone actually use it? The vast majority of crypto users, including those who work in the crypto industry, have never written a single line of Solidity code. They don’t know how the code looks nor do they care.

The Ethereum documentation uses a vending machine analogy when describing smart contracts. A vending machine has logic for accepting some inputs (eg. A3) and returning a certain output (a Kit Kat bar). Does the user of a vending machine need to know about how the vending machine is programmed? No, they just want to press a button, get a snack, and go about their day.

Likewise, we need to provide a user-friendly interface for our Calendar dapp. We want the UI to be as easy to use as Google Calendar. We will need to build a few components:

  1. Connect wallet - the user needs to connect a wallet to use the application
  2. View calendar - once a wallet is connected, the user can view the calendar
  3. Schedule appointment - the user can click on a timeslot to schedule a meeting

In this section, we will focus on the first part: connecting a wallet.

React: JavaScript Library for User Interfaces

ReactJS is a popular UI library that is used by apps like Instagram, Airbnb, Netflix, Dropbox, and Discord. It is also a very popular choice for web3 user interfaces due to its large ecosystem, component libraries, and the ability to use it for cross platform mobile applications.

Create React App

Since we already set up nodejs when creating our hardhat project, we are just a command away from having a react application up and running. Per the Create React App documentation, we simply need to run the create-react-app command with npx. We will do this in the top level of our project, which currently has this directory structure:

tree -L 1
.
├── README.md
├── artifacts
├── cache
├── contracts
├── hardhat.config.js
├── node_modules
├── package-lock.json
├── package.json
├── scripts
└── test

Running the following command will initialize a React application in a new directory named frontend.

npx create-react-app frontend

This will install some packages and set up a directory structure for your frontend:

Creating a new React app in /Users/larry/Projects/web3-tutorial/calend3/frontend.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...

Once setup has completed, we go into the frontend/ directory and do npm start:

cd frontend
npm start

This will start a local nodejs server that runs your frontend on port 3000. To see it, open your browser to http://localhost:3000. You should see the default screen with the React logo:

React logo screen

Let’s take a look at the JavaScript code that renders this screen. Open frontend/src/App.js in your editor:

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;

At the top, we import a couple of external files: the logo image and a stylesheet.

After that, we see a component called App. A component is just a function that returns some JSX. JSX is a special syntax that React uses. This syntax looks similar to HTML, but React also allows you create and render custom components.

Connect Wallet Screen

Let’s change the default App screen and make it our own. The UI is where you can show your creativity and make your app eyecatching and unique. Let’s include the title of our app and a slogan. Below that, let’s include an HTML button tag and label is “Connect Wallet”.

Update App.js

import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>calend3</h1>
        <p id="slogan">web3 appointment scheduler</p>
      </header>
      <button>connect wallet</button>
    </div>
  );
}

export default App;

Update Styles in App.css

body {
  text-align: center;
  background-color: #282c34;
  color: white;
  margin: 50px;
}

h1 {
  margin: 0;
  font-size: 5em;
}

#slogan {
  margin: 0;
  font-size: 2em;
}

button {
  background: black;
  color: white;
  border: 0;
  font-size: 1.5em;
  padding: 12px;
  cursor: pointer;
  margin-top: 30px;
}

Events

We will first present the user with the Connect Wallet screen. We then wait for the user to interact with it and respond appropriately. We do this by listening for events that are of interest to us. An event is just something that happens. Here are some examples of events:

  1. The user moved the mouse
  2. The user resized the window
  3. The user clicked a button
  4. The user typed some text into a field

In our case, we are specifically interested in when the user clicks the “Connect Wallet” button. When they click this button, we want to trigger their Metamask wallet. How do we do this? Let’s add a function called connect to our App. To call this function we’ll add an onClick attribute to our “Connect Wallet” button:

function App() {

  const connect = async () => {
    try {
      console.log('clicked connect wallet');
    } catch (error) {
      console.log(error);
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>calend3</h1>
        <p id="slogan">web3 appointment scheduler</p>
      </header>
      <button onClick={connect}>connect wallet</button>
    </div>
  );
}

export default App;

Now when you click “Connect Wallet”, you should see “clicked connect wallet” in your JavaScript Developer Console.

Provider and window.ethereum

So a console message is great and all, but how do we actually interact with MetaMask when the button is clicked? We need to use the Ethereum Provider.

According to the Ethereum Provider API documentation:

MetaMask injects a global API into websites visited by its users at window.ethereum. This API allows websites to request users' Ethereum accounts, read data from blockchains the user is connected to, and suggest that the user sign messages and transactions. The presence of the provider object indicates an Ethereum user.

You can access this object at window.ethereum. If you have MetaMask installed and type window.ethereum in the JavaScript console, you should see a JavaScript object. The MetaMask documentation recommends using a package called @metamask/detect-provider to detect the provider.

Let’s go ahead and install this package in the frontend/ directory of our application:

cd calend3/frontend
npm install @metamask/detect-provider

Now that it’s installed, let’s import it and use it in our connect() function.

import './App.css';
import detectEthereumProvider from '@metamask/detect-provider';

function App() {
  const connect = async () => {
    try {
      const provider = await detectEthereumProvider();
      
      // returns an array of accounts
      const accounts = await provider.request({ method: "eth_requestAccounts" });
      
      // check if array at least one element
      if (accounts.length > 0) {
        console.log('account found', accounts);
      } else {
        console.log('No account found');
      }
    } catch (error) {
      console.log(error);
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>calend3</h1>
        <p id="slogan">web3 appointment scheduler</p>
      </header>
      <button onClick={connect}>connect wallet</button>
    </div>
  );
}

export default App;

Pay special attention to the line below.

const accounts = await provider.request({ method: "eth_requestAccounts" });

The method eth_requestAccounts is part of EIP-1102:

Calling this method may trigger a user interface that allows the user to approve or reject account access for a given dapp.

So we use the Ethereum Provider to submit an RPC Request. This triggers MetaMask to prompt the user to connect their account and provide an Ethereum address to be identified by.

Metamask Connect

Metamask Allow

So now we have successfully connected our wallet, but nothing really happened on our user interface. We still see the “Connect Wallet” button on the screen. Why? Well, we didn’t update the state of our user interface. We want to show a state, wait for events, respond to those events, and update the interface to a new state. Our user interface is “react”-ing to events and updating the state of the user interface.

We now want to represent the state of our application when a user has their wallet connected. In this state, what do we want to see? The owner’s calendar. Let’s render a Calendar component when a user has connected their wallet.

Connected State and Hooks

What if the user is already connected? Then we don’t need to waste their time with a connect wallet button. Same as if a user is logged in aleady, we don’t show them the login button. We show them what they can do when they are logged in. We need to create a state variable.

What is a Hook?

Prior to 2018, React used classes and a function called setState() to update the state of the user interface. But now it is recommended to use hooks to update state. Hooks are a bit confusing at first, but will be easier to understand once we use them.

The useState() Hook

The first hook we’ll discuss is useState(). The syntax and usage may be a little different than what you are accustomed to, but you get used to it. The useState() hook takes a single input: the initial state. It returns: a variable that holds the state and a function to update this variable. For example, in our application, we we will add the following useState() line:

const [account, setAccount] = useState(false);
// import useState
import { useState } from 'react';
import './App.css';
import Calendar from './components/Calendar';

function App() {
  const [account, setAccount] = useState(false);

  const connect = async () => {
    try {
      const provider = await detectEthereumProvider();

This creates a state variable named account that is initialized to false. This also creates a function called setAccount() that we can use to set the variable account. So if I wrote setAccount(“Larry”), then account would change values from false to “Larry”.

How will we use this? When the UI loads, account will be false. When the user connects their wallet, we will use setAccount() to…..set account. The account variable will contain the user’s account object. So if account is not set, we’ll show the Connect Wallet button. When the user clicks Connect Wallet, we will use setAccount to update the state with the user’s account details. Once account is set, we will show a Calendar component.

import { useState } from 'react';
import './App.css';
import detectEthereumProvider from '@metamask/detect-provider';

function App() {
  const [account, setAccount] = useState(false);

  const connect = async () => {
    try {
      const provider = await detectEthereumProvider();
      
      // returns an array of accounts
      const accounts = await provider.request({ method: "eth_requestAccounts" });
      
      // check if array at least one element. if so, set account to the first one.
      if (accounts.length > 0) {
        setAccount(accounts[0]);
      } else {
        alert('No account found');
      }
    } catch (error) {
      console.log(error);
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>calend3</h1>
        <p id="slogan">web3 appointment scheduler</p>
      </header>
      {!account && <button onClick={connect}>connect wallet</button>}
      {account && <div>show calendar component</div>}
    </div>
  );
}

export default App;

Creating a Placeholder Calendar Component

We will create a sophisticated Calendar component in the next section. For now, we just want to create a skeleton/shell so that we can test switching to another state.

Create a directory called components in your frontend/src directory. In the _frontend/src/components directory, create a new file named Calendar.js.

const Calendar = () => {
    return <div id="calendar">
        Calendar
    </div>;
}

export default Calendar;
#calendar {
  background: white;
  color: black;
  font-size: 24px;
  padding: 20px;
  margin: 20px;
  height: 600px;
}

Now we will import our Calendar component into our App.js and render it in the App() component by using in rendered markup. We will render the conditionally and only show it if the account is set in the state. Likewise we can show the Connect Wallet button conditionally and only show it when the account is not set.

import { useState } from 'react';
import './App.css';
// import the Calendar component
import Calendar from './components/Calendar';

function App() {
  const [account, setAccount] = useState(false);

  const connect = async () => {
    try {
      const provider = await detectEthereumProvider();
      
      // returns an array of accounts
      const accounts = await provider.request({ method: "eth_requestAccounts" });
      
      // check if array at least one element
      if (accounts.length > 0) {
        setAccount(accounts[0]);
      } else {
        alert('No account found');
      }
    } catch (error) {
      console.log(error);
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>calend3</h1>
        <p id="slogan">web3 appointment scheduler</p>
      </header>
      {!account && <button onClick={connect}>connect wallet</button>}
      {account && <Calendar />}
    </div>
  );
}

export default App;

useEffect Hook

There is one more case that we haven’t handled yet. What if the user revisits our app after they have previous connected their wallet? We don’t want them to have to click Connect Wallet again. It should go straight to the Calendar since they have already identified themselves previously. We want to check if they have already connected their wallet when the page loads.

What is useEffect?

useEffect is a hook where you can write code that needs to run after the App renders. In this case, after we load the app, we need to check if the user is connected.

eth_accounts vs. eth_requestAccounts

The method eth_requestAccounts prompts the user to choose an Ethereum account. In the case of user that has already connected to our dapp before, we don’t need to prompt them. We simply want to request the accounts that are already connected. So we will use eth_accounts, which simply returns a list of addresses owned by client.

We will define a new function called isConnected() that checks if the user is already connected. To check this condition on page load, we will call the isConnected() function in the useEffect hook.

import { useState, useEffect } from 'react';
import './App.css';
import Calendar from './components/Calendar';
import detectEthereumProvider from '@metamask/detect-provider';

function App() {
  const [account, setAccount] = useState(false);

  useEffect(() => {
    isConnected();
  }, []);

  const isConnected = async () => {
    const provider = await detectEthereumProvider();
    const accounts = await provider.request({ method: "eth_accounts" });

    if (accounts.length > 0) {
      setAccount(accounts[0]);
    } else {
      console.log("No authorized account found")
    }
  }

  const connect = async () => {
    try {
      const provider = await detectEthereumProvider();
      
      // returns an array of accounts
      const accounts = await provider.request({ method: "eth_requestAccounts" });
      
      // check if array at least one element. if so, set account to the first one.
      if (accounts.length > 0) {
        setAccount(accounts[0]);
      } else {
        alert('No account found');
      }
    } catch (error) {
      console.log(error);
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>calend3</h1>
        <p id="slogan">web3 appointment scheduler</p>
      </header>
      {!account && <button onClick={connect}>connect wallet</button>}
      {account && <Calendar />}
    </div>
  );
}

export default App;

Next Steps: Appointments

Congrats! We now have a Connect Wallet screen and a way to identify the user’s Ethereum account. Now that we can identify a connected Ethereum address, we can add appointment scheduling functionality.

In the next section, we will add appointment data structures and scheduling functions to our smart contract. We will also discuss sending and receiving payments in Ether. See you in the next section :).