Metamask Integration – Connecting to Metamask Using React.js and Next.js Without Refreshing

ethereumjsmetamaskreactsoliditytruffle-contract

StackShare question

Hello, I am having a problem with connecting to Metamask dynamically.
I created a voting website using React.js and Next.js, and was using UseEffect() to connect to Metamask.
Currently, when I navigate to a page with its link or make any changes to a one, I fail to connect to my smart contract, although I can connect when I refresh manually. I believe that this is due to Next.js preloading data.

For instance, this is what I see when navigating to index.js :
enter image description here

However, everything usually works after I refresh the page

enter image description here
Here is a basic implementation of my index.js

  useEffect(() => {// get web3
    async function initWeb3() {
      const web3Instance = await getWeb3();
      setWeb3(web3Instance)
    }
    initWeb3();
  },[]);

  useEffect(() => {// get Factory contract
    async function setup() {
      if(web3 == "") {
        console.log('unable to get factory')
        return;
      }
      try {
        // Get the contract instance.
        const networkId = await web3.eth.net.getId();
        const deployedNetwork = VoteFactoryContract.networks[networkId];
        const instance = new web3.eth.Contract(
          VoteFactoryContract.abi,
          deployedNetwork && deployedNetwork.address,
        );
        setContract(instance);// Set web3, accounts, and contract to the state, and then proceed with an
      } catch (error) {
        // Catch any errors for any of the above operations.
        alert(
          `Failed to load web3, accounts, or contract. Check console for details.`,
);
        console.error(error);
      }
    }
      setup();
  },[web3]);

  useEffect(()=> {//display available votes addresses
    var displayVotes = async () => {
      if(contract == ''){
        return;
      }
      const response = await contract.methods.getDeployedVotes().call();
      // Update state with the result.
      setVotesAddresses(response);
    };
    displayVotes();
  },[contract]);

getWeb3() looks like this:

import Web3 from "web3";

const getWeb3 = () =>
  new Promise((resolve, reject) => {
    // Wait for loading completion to avoid race conditions with web3 injection timing.
    window.addEventListener("load", async () => {
      // Modern dapp browsers...
      if (window.ethereum) {
        const web3 = new Web3(window.ethereum);
        try {
          // Request account access if needed
          await window.eth_requestAccounts;
          // Acccounts now exposed
          resolve(web3);
        } catch (error) {
          reject(error);
        }
      }
      // Legacy dapp browsers...
      else if (window.web3) {
        // Use Mist/MetaMask's provider.
        const web3 = window.web3;
        console.log("Injected web3 detected.");
        resolve(web3);
      }
      // Fallback to localhost; use dev console port by default...
      else {
        const provider = new Web3.providers.HttpProvider(
          "http://127.0.0.1:8545"
        );
        const web3 = new Web3(provider);
        console.log("No web3 instance injected, using Local web3.");
        resolve(web3);
      }
    });
  });

export default getWeb3;

Finally, my contract code is as follows:

pragma solidity ^0.7.4;
//"SPDX-License-Identifier: UNLICENSED"

import "./Vote.sol";
 
contract VoteFactory{
    address[] public deployedVotes;
    
    function createVote(uint typeOf) public{
        address newVote = address(new Vote(msg.sender, typeOf));
        deployedVotes.push(newVote);
    }

    function getDeployedVotes() public view returns (address[] memory) {
        return deployedVotes;
    }
    
}

I have spent endless hours trying to debug this, and although I feel like I know what the error is, I cannot find a solution.

Best Answer

This is probably because you are establishing the web3 connection only in your index.js; therefore, it is not accessible when you navigate into other pages.

You can either use Redux to have global variables accessible from any page, or share data between components through properties.

** UPDATE: **

In case it helps, this is the way I normally connect to web3 through React (in Typescript):

React.useEffect(() => {
    const init = async () => {
        await getWeb3(true);
    }
    init();
}, []);

const getWeb3 = async (isFirstLoad: boolean) => {
    try {
        let web3: Web3;
        if (window.ethereum) {
            web3 = new Web3(window.ethereum);
            // Ask User permission to connect to Metamask
            if (!isFirstLoad) {
                try {
                    await window.ethereum.enable();
                } catch (err) {
                    console.log('Transaction rejected by user:', err)
                };
            };
        } else if (window.web3) {
            web3 = new Web3(window.web3.currentProvider);
        } else {
            window.alert('Non-Ethereum browser detected. Please install MetaMask plugin');
            return;
        };

        dispatch(setWeb3(web3));  // Update web3 into Redux state

        // ...
    } catch (err) {
        console.log('Error in Web3.tsx -> getWeb3(): ', err);
    };
};

The variable isFirstLoad prevents asking the User to reconnect to your site if he/she was already connected.

I had some issues when using window.addEventListener(), so perhaps you can try without it.

Related Topic