Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Connecting MetaMask to Your DApp

Hello! Welcome to the next lesson in our dApp frontend module.

In the last lesson, we successfully established a foundational connection between our React application and the Ethereum network. We created a "Connect Wallet" button, fetched the user's address, and even made the connection robust by listening for account changes.

Today, we'll build on that foundation to implement more sophisticated wallet connection logic. Our goal is to move from a basic proof-of-concept to a more practical and user-friendly implementation. We will refactor our logic into a dedicated component, fetch and display the user's ETH balance, and ensure our application gracefully handles events like the user switching accounts or networks within MetaMask. This is a key step in creating a polished user experience for any dApp.

1. The Need for Component-Based Architecture

In our previous lesson, we put all the connection logic directly into App.tsx. While this worked, in a real-world application, it's better to organize code into smaller, reusable components. Given your background in backend development, you can think of this as applying the Single Responsibility Principle: we'll create one component whose sole job is to manage the wallet connection and display its state.

This approach keeps our main App.tsx file clean and makes the wallet connection logic easier to manage, debug, and even reuse elsewhere. We'll create a WalletCard component that will handle:

  • Displaying a "Connect Wallet" button.
  • Prompting the user to connect with MetaMask.
  • Displaying the connected account address and ETH balance.
  • Dynamically updating when the user changes their account or network in MetaMask.
MetaMask Connection Prompt for a dApp
This is the standard MetaMask prompt our connection logic will trigger, asking the user for permission to connect their account to our dApp.

2. Building the WalletCard Component: A Video Guide

We're going to follow a video tutorial that walks through building a component very similar to what we need. It will demonstrate how to structure the state, handle the connection, fetch the balance, and listen for important wallet events.

This video uses JavaScript, but the principles are identical. We will adapt the final code to our TypeScript project afterwards.

Ethereum Dapp: Connect React app to MetaMask Tutorial

The video 'Ethereum Dapp: Connect React app to MetaMask Tutorial' by Michael Couch provides an excellent walkthrough for building a self-contained wallet connection component. We'll watch it in segments.

We will break down this video into key parts. I will guide you on which segments to watch and then we'll discuss the concepts.

Step 2.1: Setting up the Component and Connection Logic

First, let's see how to structure the React component and write the core function that handles the wallet connection request.

Ethereum Dapp: Connect React app to MetaMask Tutorial

Watch this segment to understand how to set up the component's UI and state, and how to implement the connectWalletHandler function.

Please watch from 03:01 to 09:46. Focus on: How useState is used to manage different pieces of information (account, balance, error messages). The logic inside connectWalletHandler, specifically how it checks for window.ethereum and uses eth_requestAccounts to prompt MetaMask.

The video demonstrates the core connection flow:

  1. Check for MetaMask: The function first checks if window.ethereum exists. If not, it sets an error message prompting the user to install MetaMask.
  2. Request Accounts: It then calls window.ethereum.request({ method: 'eth_requestAccounts' }). This is the JSON-RPC method that asks the user to connect their wallet. It returns a promise that resolves with an array of the user's account addresses.
  3. Update State: Once the account is received, a handler function (accountChangedHandler in the video) is called to update the component's state with the new address, causing the UI to re-render and display it.

Step 2.2: Fetching the User's Balance

Just having the address is useful, but users expect to see their balance. Let's look at how to query the blockchain for this information.

Ethereum Dapp: Connect React app to MetaMask Tutorial

This next clip shows how to fetch the connected account's ETH balance and format it for display.

Now, watch from 09:46 to 12:23. Pay close attention to: The eth_getBalance RPC method and its parameters. The format of the raw balance returned from the blockchain. The use of ethers.utils.formatEther to convert the balance into a human-readable format.

Fetching the balance involves two key steps:

  1. Request Balance: We use another RPC method, eth_getBalance, passing the user's address and the block tag 'latest' to get the most recent balance.
  2. Format Balance: The balance is returned as a large integer string representing the amount in Wei (the smallest denomination of Ether, Wei = 1 ETH). This is not user-friendly. The ethers library provides a handy utility, ethers.utils.formatEther(), which correctly converts this Wei value into an ETH value as a string, which we can then display.

Step 2.3: Handling Wallet Events

A robust dApp must react when the user interacts with their wallet. The previous lesson introduced this, and this video reinforces the pattern in a clean, component-based way.

Ethereum Dapp: Connect React app to MetaMask Tutorial

Finally, let's see how to make our component dynamically respond to the user switching accounts or networks in MetaMask.

Watch the final segment from 12:23 to 17:00. This covers listening for the accountsChanged and chainChanged events.

The key takeaways here are the two event listeners:

  • window.ethereum.on('accountsChanged', ...): This event fires when the user switches their active account. The handler function receives the new account address, allowing us to update the UI and re-fetch the new account's balance automatically.
  • window.ethereum.on('chainChanged', ...): This event fires when the user switches networks (e.g., from Sepolia to Mainnet). The safest and most common way to handle this is to force a page reload (window.location.reload()). This ensures your dApp re-initializes its state and connection, preventing it from trying to interact with the wrong network.

3. Implementing the WalletCard in TypeScript

Now, let's translate the concepts from the video into a complete WalletCard.tsx component for our project.

  1. Create a new file in your src/ directory named WalletCard.tsx.
  2. Paste the following code into the new file. This code adapts the video's logic to TypeScript and uses the useEffect hook to manage the event listeners, as we learned in the previous lesson.
// src/WalletCard.tsx

import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import './WalletCard.css';

const WalletCard = () => {
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const [account, setAccount] = useState<string | null>(null);
  const [balance, setBalance] = useState<string | null>(null);

  useEffect(() => {
    // Listener for account changes
    const accountWasChanged = (accounts: string[]) => {
      if (accounts.length > 0) {
        setAccount(accounts[0]);
        getAccountBalance(accounts[0]);
      } else {
        setAccount(null);
        setBalance(null);
      }
    }

    // Listener for chain changes
    const chainWasChanged = () => {
      // A simple reload is a robust way to handle network changes
      window.location.reload();
    }

    if (window.ethereum) {
      // Check for existing connection on page load
      connectHandler(); 

      window.ethereum.on('accountsChanged', accountWasChanged);
      window.ethereum.on('chainChanged', chainWasChanged);
    }
    
    // Cleanup function to remove listeners
    return () => {
      if (window.ethereum) {
        window.ethereum.removeListener('accountsChanged', accountWasChanged);
        window.ethereum.removeListener('chainChanged', chainWasChanged);
      }
    };
  }, []);

  const connectHandler = async () => {
    if (window.ethereum) {
      try {
        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        if (accounts.length > 0) {
          setAccount(accounts[0]);
          getAccountBalance(accounts[0]);
        }
      } catch (err: any) {
        setErrorMessage(err.message);
      }
    } else {
      setErrorMessage('Please install MetaMask to use this dApp.');
    }
  };

  const getAccountBalance = async (accountAddress: string) => {
    if (window.ethereum) {
      try {
        const provider = new ethers.BrowserProvider(window.ethereum);
        const accountBalance = await provider.getBalance(accountAddress);
        setBalance(ethers.formatEther(accountBalance));
      } catch (err: any) {
        setErrorMessage(err.message);
      }
    }
  };

  return (
    <div className='wallet-card'>
      <h3 className='wallet-card-title'>Wallet Connection</h3>
      {account ? (
        <div>
          <p><strong>Address:</strong> {`${account.substring(0, 6)}...${account.substring(account.length - 4)}`}</p>
          <p><strong>Balance:</strong> {balance ? `${parseFloat(balance).toFixed(4)} ETH` : 'Loading...'}</p>
        </div>
      ) : (
        <button className='connect-wallet-button' onClick={connectHandler}>
          Connect Wallet
        </button>
      )}
      {errorMessage && <p className='error-message'>{errorMessage}</p>}
    </div>
  );
};

export default WalletCard;
  1. Create a corresponding CSS file src/WalletCard.css for styling:
/* src/WalletCard.css */

.wallet-card {
  border: 1px solid #ccc;
  border-radius: 8px;
  padding: 20px;
  width: 350px;
  background-color: #f9f9f9;
  box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}

.wallet-card-title {
  margin-top: 0;
  color: #333;
}

.connect-wallet-button {
  width: 100%;
  padding: 12px 24px;
  font-size: 16px;
  font-weight: bold;
  color: white;
  background-color: #007bff;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.connect-wallet-button:hover {
  background-color: #0056b3;
}

.error-message {
  color: #dc3545;
  margin-top: 15px;
  word-wrap: break-word;
}
  1. Finally, update your src/App.tsx to use this new component, cleaning it up significantly:
// src/App.tsx

import './App.css';
import WalletCard from './WalletCard';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>My Token dApp</h1>
        <WalletCard />
      </header>
    </div>
  );
}

export default App;

Now, run your app with npm run dev. You should see the new WalletCard component. It will prompt you to connect, and once connected, it will display your truncated address and formatted ETH balance. Try switching accounts in MetaMask—the UI should update automatically!

Test your understanding!

In the last lesson, we used provider.getSigner() to prompt the user to connect. In this lesson's code and video, window.ethereum.request({ method: 'eth_requestAccounts' }) is used. Both seem to achieve the same result of bringing up the MetaMask connection prompt. What is the key difference between these two approaches?

Show answer

The main difference lies in their level of abstraction.

  • window.ethereum.request({ method: 'eth_requestAccounts' }) is a direct call to the low-level JSON-RPC API that MetaMask injects into the browser (window.ethereum). Its primary job is to request account access, and it returns an array of account strings.

  • provider.getSigner() is a higher-level abstraction provided by the ethers.js library. When you call it, ethers internally uses eth_requestAccounts to get the account, but it doesn't just return the address string. It returns a Signer object. This Signer is already configured and ready to be used for "write" operations, like signing and sending transactions.

In short: eth_requestAccounts gets you the account address, while getSigner() gets you an object that represents the account and can perform actions on its behalf. We'll be using the Signer object extensively in future lessons to interact with our smart contracts.

Conclusion

Excellent work! You've taken our basic wallet connection and transformed it into a robust, reusable, and user-friendly component. This is a crucial pattern for building clean and maintainable dApps.

Key Takeaways:

  • Encapsulating logic into dedicated React components (like WalletCard) improves code organization and reusability.
  • You can fetch a user's native token balance using provider.getBalance() (or the eth_getBalance RPC method).
  • Balances returned from the blockchain are in Wei and must be formatted using utilities like ethers.formatEther() for display.
  • Combining useState and useEffect with event listeners for accountsChanged and chainChanged creates a dApp that feels responsive and reliable to the end-user.

Preview of the Next Lesson:
Our connection logic is now solid. The next step is to start interacting with our own smart contract. In the next lesson, we will read and display data from your ERC-20 token contract. We'll learn how to create an ethers.js contract instance and use it to fetch and display your token's name, symbol, and total supply right in our dApp.

Can't find a good explanation? Sign up and we'll make it for you

Sign up