A React Component to connect with MetaMask

Giancarlo Buomprisco
·
·6 min read

Web3 is a relatively new, revolutionary (maybe), and controversial (very) phenomenon, considered the evolution of the Web as we know it today, and becoming harder and harder to ignore.

Whether you are fascinated by it or not, it's undeniable that its consequences are far-reaching; many developers are re-training to get up to speed with it, and the buzz around it has never been this high.

We are not here to discuss its qualities but show how to start working with Web3 websites using the MetaMask wallet.


Recently, I started working on a task for requesting users to connect their wallets to retrieve their MetaMask address; I had the opportunity to get my hands dirty with Web3 technologies for the first time.

What's a Wallet?

If you do not know what MetaMask is, here is a short explanation: MetaMask is a digital wallet living in your browser as an extension; you can use it to keep, transfer or move various cryptocurrencies and NFTs.

Furthermore, web3 websites can request to connect with said wallet, and if authorized, perform actions on behalf of the owners.

For example, you can connect the wallet to OpenSea (or any other marketplace) and buy an NFT, or connect to PancakeSwap to swap tokens with other ones.

Building the Component

While being a blockchain engineer involves writing custom smart contracts, protocols, etc., it's slightly ironic that web3, as revolutionary as claimed, will still be primarily built using web2 technologies.

That means tons of Javascript (which is a little bit embarrassing to acknowledge when hacks happen due to this fact).

In short: we still need the good old Javascript to build on top of web3, so let's see how we can get started with a simple component, which, thanks to the MetaMask wallet, allows us to request the user's account of choice using the API exposed by the Chrome extension.

Extending Window

We need to extend the Window with the MetaMask API as we use Typescript.

The extension exposes an object named ethereum, which provides the necessary API to interact with the wallet.

We will add the script below globally or within the same Component's file:

declare global {
  interface Window {
    ethereum: {
      request<T>(params: { method: string }): Promise<T>;
      on<T>(event: string, cb: (params: T) => void): void;
      removeListener<T>(event: string, cb: (params: T) => void): void;
      selectedAddress: string | undefined;
    };
  }
}

Component Basics

And now we can start writing the component.

Let's layout a basic ConnectWalletButton component:

import React from "react";

const ConnectWalletButton: React.FC<{
  onConnect: (address: string | undefined) => void
}> = ({ onConnect }) => {
  // implementation
};

export default ConnectWalletButton;

And now we can import it in our App:

import "./styles.css";
import ConnectWalletButton from "./ConnectWalletButton";

export default function App() {
  const [selectedAddress, setSelectedAddress] = useState<string>();
  
  const addressChanged = useCallback((address: string | undefined) => {
    setSelectedAddress(address);
  }, []);

  return (
    <div className="App">
      <h1>Connect with MetaMask Demo</h1>

      <ConnectWalletButton onChange={addressChanged} />
    </div>
  );
}

We defined a callback to read the address from the button's parent.

The above is a decent solution if you need to read the address locally. Otherwise, should you need to read it globally, it would be better to use any state-management tool like the Context API or more complex stuff like Zustand, Redux, etc.

Let's add just enough CSS to make it look good-ish:

button {
  padding: 0.75rem 1.5rem;
  border-radius: 30px;
  font-size: 1rem;
  background-color: #03a9f4;
  color: #fff;
  font-weight: 600;
  border-width: 0;
  cursor: pointer;
}

button:hover {
  background-color: #039be5;
}

button:active {
  background-color: #0288d1;
}

Using the MetaMask Ethereum API

And now on to the actual logic.

If you paid attention when we extended Window, we have added two methods to the ethereum global object (just enough to make this component work because there are many more).

  • The first method is request: by calling it, we can request actions, which in this case is eth_requestAccounts
  • The second method is on: by calling it, we can listen to specific events, for example, when the wallet's account has changed
  • The third method removeListener is used to clean up the callback and avoid memory leaks when the component is unmounted

Goals

What will the component do?

  • Prompt users to Connect their wallet
  • Display the address if connected
  • Emit an event with the address when it changes
  • Automatically update if the user updates the address or disconnects from MetaMask

Implementation

Let's start implementing the goals above.

First, we want to check if the user installed the MetaMask extension; otherwise, we'll run into runtime errors. For simplicity, we check if window.ethereum exists:

function isMetaMaskInstalled() {
  return Boolean(window.ethereum);
}

Then, we define an async function which lets us request and read the address from MetaMask:

async function readAddress() {
  const method = "eth_requestAccounts";

  const accounts = await window.ethereum.request<string[]>({
    method
  });

  // for simplicity, let's return the first account in the list
  return accounts[0];
}

function getSelectedAddress() {
  return window.ethereum?.selectedAddress;
}

We want to store the address because it can also mutate outside.

Because the wallet may already be connected when the component mounts, we initialize it with the property selectedAddress, if found:

const [address, setAddress] = useState<string>(
  getSelectedAddress()
);

Let's define a function which will read and store the address:

const connectWallet = async () => {
  const selectedAddress = await readAddress();

  setAddress(selectedAddress);
  onChange(selectedAddress);
};

We now need to listen for changes from the extension to update the component accordingly.

In fact, users can change the current address or disconnect the website using the MetaMask extension:

useEffect(() => {
  const eventName = `accountsChanged`;
  
  // break if metamask not installed
  if (!isMetaMaskInstalled()) return;

  const listener = ([selectedAddress]: string[]) => {
    setAddress(selectedAddress);
    onChange(selectedAddress);
  };

  // listen for updates
  window.ethereum.on(eventName, listener);
  
  // clean up on unmount
  return () => {
    window.ethereum.removeListener(eventName, listener);
  };
}, []);

What happens here?

  • we listen for updates using the on method, and update the current address using the first element in the list
  • when the component unmounts, we call the removeListener method so we can clean everything up

Now, we can finally render our component.

If MetaMask is not installed, we warn user they cannot connect, and need to install the extension first:

if (!isMetaMaskInstalled()) {
  return <span>No wallet found. Please install MetaMask.<span/>;
}

If a wallet is connected, we simply display its selected address:

if (address) {
  return <button>Connected with {address}</button>;
}

If a wallet is not connected, we prompt to connect:

return <button onClick={connectWallet}>Connect Wallet</button>;

Demo

Do you have a MetaMask wallet?

Awesome, you can try the demo below to try out the code we've written! Also, you can find the complete snippet:

CodeSandbox Demo

Final Words

I know, I know. Web3 is controversial.

I have some reservations myself, but with that said, I also feel like it's good to keep an open mind about the technology and how it's shaping up the new tools we're going to use in the future probably.

I hope to make more posts about this as I dig further into the space.

To the next one, ciao!