Recommended resources
The freely available resources this lesson will direct you to.
- Integrate React.js with Smart Contracts (read / write / events)www.youtube.com
- Full-Stack Dapp using Solidity, Ether.js, Hardhat, and React JS | Code Eater - Blockchain | Englishwww.youtube.com
- Creating a “Connect Wallet” Button Using ethers.js v6 in ...medium.com
- Into the Ether: Wallet Integration on Ethereum Using Ethers.js v6mark-okeeffe-11887.medium.com
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'swindowobject calledethereum. 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
Provideris 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 useethers.BrowserProviderto wrap thewindow.ethereumobject. -
Signer: A
Signeris 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").
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:
import { useState } from 'react';: We import React'suseStatehook to manage our component's state.import { ethers } from 'ethers';: We import theetherslibrary.const [walletAddress, setWalletAddress] = useState<string>("");: We define a state variablewalletAddressto store the user's address. It's a string, initially empty. Your TypeScript background makes the use of the generic<string>here familiar.async function requestAccount(): This is our main function. It'sasyncbecause interacting with the blockchain and wallet is asynchronous.if (window.ethereum): This is a crucial check to see if the user has MetaMask or another EIP-1193 compatible wallet.const provider = new ethers.BrowserProvider(window.ethereum);: We create ourProviderby wrapping the injectedwindow.ethereumobject.const signer = await provider.getSigner();: This is the magic step. Ethers asks the provider for aSigner. 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.const address = await signer.getAddress();: Once we have the signer, we can get the associated address.setWalletAddress(address);: We update our React state with the fetched address, which causes the UI to re-render and display it.- Conditional Rendering: In the JSX, the button's text changes based on whether
walletAddressis 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:
useEffecthook: This hook runs code after the component renders. With an empty dependency array ([]), it runs only once when the component mounts.accountsChangedListener: We attach a functionhandleAccountsChangedto theaccountsChangedevent. 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.- Cleanup Function: The function returned from
useEffectis a cleanup function. It runs when the component unmounts, removing the event listener to prevent memory leaks. checkIfWalletIsConnected: We added a function that runs on page load to see if the dApp is already connected. Instead of usinggetSigner()(which would prompt the user), we useprovider.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.ethereumobject injected by wallets like MetaMask. - The
Providerin ethers.js is for read-only access, while theSigneris for write access (signing transactions). - You trigger a connection prompt by requesting a
Signerwithprovider.getSigner(). - The
useEffecthook is the perfect place to set up event listeners foraccountsChangedandchainChangedto 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.