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 to Ethereum with Ethers.js

Hello! Welcome back to Module 9.

In our last lesson, we set up a clean, modern React and TypeScript project using Vite. We now have a blank canvas and the essential ethers.js library installed. Today, we'll take the first and most exciting step in bringing our dApp to life: we will integrate ethers.js to connect our frontend to the Ethereum network.

Our goal is to build the "Connect Wallet" button that is the entry point for almost every dApp. By the end of this lesson, you'll have a button that prompts a user to connect with MetaMask, retrieves their wallet address, and displays it in the application. This bridges the gap between your user interface and the blockchain.

1. The Core Concepts: Provider, Signer, and window.ethereum

Before we write any code, let's understand the key players involved in connecting a web application to the Ethereum blockchain. Your backend experience with client-server communication will be a useful parallel here.

  • window.ethereum: When a user has a browser wallet like MetaMask installed, the wallet "injects" a special object into the browser's window object called ethereum. This object is our gateway to the blockchain. It allows our dApp to request user accounts, suggest new networks, and ask the user to sign transactions. If this object doesn't exist, it means the user doesn't have a compatible wallet installed.

  • Provider: In ethers.js, a Provider is a read-only connection to the Ethereum blockchain. It allows you to query the blockchain's state, like getting account balances, reading data from smart contracts, or looking up transaction details. Think of it as a connection to a public, read-only API endpoint for the blockchain. We will use ethers.BrowserProvider to wrap the window.ethereum object.

  • Signer: A Signer is an object that represents an Ethereum account. Unlike a Provider, a Signer has the ability to perform actions that change the state of the blockchain (i.e., write operations). This requires the user's private key to "sign" transactions, which is why a Signer is needed for sending ETH or calling state-changing functions on a smart contract. The user's wallet (MetaMask) securely manages the private key; our app never sees it. We simply ask the Signer to perform an action, and the wallet prompts the user for confirmation.

This article provides a great conceptual overview of these components.

Into the Ether: Wallet Integration on Ethereum Using Ethers.js v6

Read this brief article, 'Into the Ether: Wallet Integration on Ethereum Using Ethers.js v6', to solidify your understanding of the roles of the Provider and Signer.

Please read the sections titled 'Provider' and 'Signer'. Focus on the distinction between the read-only nature of the Provider and the read-write capabilities of the Signer.

2. High-Level Architecture

As we build our dApp, we'll create different components for different functionalities: connecting, showing balances, transferring tokens, etc. The following diagram illustrates how these pieces might eventually fit together, managed by a central state (the "Context").

dApp Frontend Wallet and Context Interaction Diagram
This diagram shows a conceptual architecture for our dApp. Today, we're building the 'Connect' functionality and displaying wallet details like the 'account' address. The connection logic we build will be the foundation for all other features.

3. Building the "Connect Wallet" Component

Now for the hands-on part. We will build a React component that contains the "Connect Wallet" button and the logic to interact with MetaMask.

The following guide provides a clear, self-contained example of exactly what we need to build. We'll use it as our primary reference for the code.

Creating a “Connect Wallet” Button Using ethers.js v6 in ...

This article, 'Creating a “Connect Wallet” Button Using ethers.js v6 in a React DApp', provides a complete code example for a wallet connection component. We will implement this in our App.tsx file.

Read through the sections '3. Create the UI' and '4. Style Your Button'. Pay close attention to the App.js code. We will be adapting this for our App.tsx file.

Let's break this down and implement it in our project.

Step 1: Update App.tsx

Replace the content of your src/App.tsx file with the following code. This is adapted from the guide you just read, with TypeScript types added.

import { useState } from 'react';
import { ethers } from 'ethers';
import './App.css';

function App() {
  // State variables
  const [walletAddress, setWalletAddress] = useState<string>("");

  // Function to connect the wallet
  async function requestAccount() {
    console.log('Requesting account...');

    // Check if MetaMask is installed
    if (window.ethereum) {
      console.log('MetaMask detected');
      try {
        const provider = new ethers.BrowserProvider(window.ethereum);
        const signer = await provider.getSigner();
        const address = await signer.getAddress();

        setWalletAddress(address);
        console.log('Wallet connected:', address);
      } catch (error) {
        console.error('Error connecting to MetaMask:', error);
      }
    } else {
      alert('MetaMask not detected! Please install it.');
    }
  }

  return (
    <div className="App">
      <header className="App-header">
        <h1>My Token dApp</h1>
        <button className="connect-wallet-button" onClick={requestAccount}>
          {walletAddress ? `Connected: ${walletAddress.substring(0, 6)}...${walletAddress.substring(walletAddress.length - 4)}` : "Connect Wallet"}
        </button>
      </header>
    </div>
  );
}

export default App;

Code Breakdown:

  1. import { useState } from 'react';: We import React's useState hook to manage our component's state.
  2. import { ethers } from 'ethers';: We import the ethers library.
  3. const [walletAddress, setWalletAddress] = useState<string>("");: We define a state variable walletAddress to store the user's address. It's a string, initially empty. Your TypeScript background makes the use of the generic <string> here familiar.
  4. async function requestAccount(): This is our main function. It's async because interacting with the blockchain and wallet is asynchronous.
  5. if (window.ethereum): This is a crucial check to see if the user has MetaMask or another EIP-1193 compatible wallet.
  6. const provider = new ethers.BrowserProvider(window.ethereum);: We create our Provider by wrapping the injected window.ethereum object.
  7. const signer = await provider.getSigner();: This is the magic step. Ethers asks the provider for a Signer. If the dApp isn't already connected, this line will trigger MetaMask to pop up and ask the user for permission to connect their account.
  8. const address = await signer.getAddress();: Once we have the signer, we can get the associated address.
  9. setWalletAddress(address);: We update our React state with the fetched address, which causes the UI to re-render and display it.
  10. Conditional Rendering: In the JSX, the button's text changes based on whether walletAddress is set. This is a common pattern for showing connection status. We also truncate the address for a cleaner display.

Step 2: Add Basic Styling

Let's add some simple CSS to make it look better. Replace the content of src/App.css with this:

.App {
  text-align: center;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.connect-wallet-button {
  margin-top: 20px;
  padding: 12px 24px;
  font-size: 16px;
  font-weight: bold;
  color: #282c34;
  background-color: #61dafb;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.2s;
}

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

Now, run your app with npm run dev. You should see your "Connect Wallet" button. Click it, and MetaMask should prompt you to connect. Once you approve, the button text will update to show your connected address!

4. Making the Connection Robust

Our button works, but a professional dApp needs to handle a few more things, like when a user switches accounts inside MetaMask. Currently, our app wouldn't know this has happened.

We can listen for events emitted by the window.ethereum object. The accountsChanged event is perfect for this.

Let's watch a short video clip that demonstrates how to implement this.

Full-Stack Dapp using Solidity, Ether.js, Hardhat, and React JS | Code Eater - Blockchain | English

This clip from the 'Full-Stack Dapp' tutorial shows how to listen for account changes in MetaMask and automatically reload the application.

Watch the section from 00:48:44 to 00:50:33. The presenter adds a listener for the 'accountsChanged' event to improve the dApp's user experience.

We can implement this using a useEffect hook in our App.tsx to set up the listener when the component first loads.

Add the useEffect hook to your App.tsx file:

import { useState, useEffect } from 'react'; // <-- Add useEffect
import { ethers } from 'ethers';
import './App.css';

function App() {
  const [walletAddress, setWalletAddress] = useState<string>("");

  useEffect(() => {
    // Check for an existing connection on page load
    checkIfWalletIsConnected();
    
    // Set up the event listener
    const handleAccountsChanged = (accounts: string[]) => {
      if (accounts.length > 0) {
        setWalletAddress(accounts[0]);
        console.log("Switched to account:", accounts[0]);
      } else {
        setWalletAddress("");
        console.log("Wallet disconnected");
      }
    };

    if (window.ethereum) {
      window.ethereum.on('accountsChanged', handleAccountsChanged);
    }
    
    // Cleanup function to remove the listener
    return () => {
      if (window.ethereum) {
        window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
      }
    };
  }, []); // Empty dependency array means this runs once on mount

  async function checkIfWalletIsConnected() {
    if (window.ethereum) {
      try {
        const provider = new ethers.BrowserProvider(window.ethereum);
        const accounts = await provider.send("eth_accounts", []);
        if (accounts.length > 0) {
          setWalletAddress(accounts[0]);
          console.log("Found connected account:", accounts[0]);
        }
      } catch (error) {
        console.error("Error checking for wallet connection:", error);
      }
    }
  }

  // ... (keep the requestAccount function from before)
  async function requestAccount() {
      // ...
  }
  
  return (
    // ... (return statement is unchanged)
  );
}

export default App;

What we added:

  1. useEffect hook: This hook runs code after the component renders. With an empty dependency array ([]), it runs only once when the component mounts.
  2. accountsChanged Listener: We attach a function handleAccountsChanged to the accountsChanged event. This function receives an array of accounts. If the user connects or switches, the array will contain the new address. If they disconnect all accounts, it will be empty. We update our state accordingly.
  3. Cleanup Function: The function returned from useEffect is a cleanup function. It runs when the component unmounts, removing the event listener to prevent memory leaks.
  4. checkIfWalletIsConnected: We added a function that runs on page load to see if the dApp is already connected. Instead of using getSigner() (which would prompt the user), we use provider.send("eth_accounts", []). This is a read-only method that returns already-approved accounts without a popup.

Now your dApp will automatically update if you switch accounts in MetaMask and will remember your connection when you refresh the page.

Test your understanding!

The accountsChanged event is crucial, but another important event is chainChanged, which fires when the user switches networks in MetaMask (e.g., from Sepolia to Ethereum Mainnet).

How would you modify the useEffect hook to also listen for chainChanged and simply reload the page when the network changes? This is a common pattern to ensure the dApp re-initializes with the correct network context.

Show answer

You would add another event listener inside the useEffect hook.

useEffect(() => {
  // ... existing code for checkIfWalletIsConnected and accountsChanged ...

  const handleChainChanged = () => {
    // For simplicity, we'll just reload the page.
    // A more advanced dApp might handle this without a full reload.
    window.location.reload();
  };

  if (window.ethereum) {
    window.ethereum.on('accountsChanged', handleAccountsChanged);
    window.ethereum.on('chainChanged', handleChainChanged); // <-- Add this listener
  }
  
  // Cleanup function
  return () => {
    if (window.ethereum) {
      window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
      window.ethereum.removeListener('chainChanged', handleChainChanged); // <-- And its cleanup
    }
  };
}, []);

Conclusion

Fantastic work today! You have successfully built the fundamental entry point for any dApp: the wallet connection. You've gone beyond a simple button by creating a robust connection that persists across page loads and reacts to changes in the user's wallet.

Key Takeaways:

  • A dApp connects to the blockchain via the window.ethereum object injected by wallets like MetaMask.
  • The Provider in ethers.js is for read-only access, while the Signer is for write access (signing transactions).
  • You trigger a connection prompt by requesting a Signer with provider.getSigner().
  • The useEffect hook is the perfect place to set up event listeners for accountsChanged and chainChanged to make your dApp responsive to user actions.

Preview of the Next Lesson:
We are now connected to the Ethereum network and have the user's address. In the next lesson, we will use this connection to read data from your smart contracts. We'll create an ethers.js instance of our deployed ERC-20 token contract and fetch its name, symbol, and total supply to display in our dApp.

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

Sign up