Home > Enterprise >  React State Fails To Update with UseEffect
React State Fails To Update with UseEffect

Time:05-14

When attempting to update an array via React state management, the state array is populated, but the user interface fails to update. The user interface only updates after I click on the navbar, and reroute to the current page (in which case useEffect does not run again, but the UI is updated).

State Code

 const[isFetched, setIsFetched] = useState(false);
  const[balances, setBalances] = useState<IBalance[]>([]);
  
  const[num, setNum] = useState(0);

  useEffect(() => {
    console.log(balances);
    // LOGS A POPULATED ARRAY
    console.log(balances.length);
    // LOGS 0
  }, [balances]);
  
  useEffect(() => {
      const fetchBalances = async() =>{
        let bals:IBalance[] = await kryptikService.getBalanceAllNetworks(kryptikWallet);
        console.log("RECIEVED BALANCES:");
        console.log(bals);
        console.log(bals.length);
        setBalances(bals);
        setIsFetched(true);
      }
      fetchBalances();
    }, []);

UI Code

      <h2>Your Balances</h2>
      <Divider/>
      {
        !isFetched?<p>Loading Balances.</p>:
        <ul role="list" className="divide-y divide-gray-200 dark:divide-gray-700">
          {balances.map((balance:IBalance) => (
              <ListItem title={balance.fullName} imgSrc={balance.iconPath} subtitle={balance.ticker} amount={balance.amountCrypto}/>
          ))}
        </ul>
      }
    </div>

Fetch Handler (called in UseEffect)

    getBalanceAllNetworks = async(walletUser:IWallet):Promise<IBalance[]> =>{
    let networksFromDb = this.getSupportedNetworkDbs();
    // initialize return array
    let balances:IBalance[] = [];
    networksFromDb.forEach(async nw => {
        let network:Network = new Network(nw.fullName, nw.ticker);
        let kryptikProvider:KryptikProvider = await this.getKryptikProviderForNetworkDb(nw);
        if(network.getNetworkfamily()==NetworkFamily.EVM){
            if(!kryptikProvider.ethProvider) throw Error(`No ethereum provider set up for ${network.fullName}.`);
            let ethNetworkProvider:JsonRpcProvider = kryptikProvider.ethProvider;
            console.log("Processing Network:")
            console.log(nw.fullName);
            // gets all addresses for network
            let allAddys:string[] = await walletUser.seedLoop.getAddresses(network);
            // gets first address for network
            let firstAddy:string = allAddys[0];
            console.log(`${nw.fullName} Addy:`);
            console.log(firstAddy);
            console.log(`Getting balance for ${nw.fullName}...`);
            // get provider for network
            let networkBalance = await ethNetworkProvider.getBalance(firstAddy);
            console.log(`${nw.fullName} Balance:`);
            console.log(networkBalance);
            // prettify ether balance
            let networkBalanceAdjusted:Number = BigNumber.from(networkBalance)
            .div(BigNumber.from("10000000000000000"))
            .toNumber() / 100;
            let networkBalanceString = networkBalanceAdjusted.toString();
            let newBalanceObj:IBalance = {fullName:nw.fullName, ticker:nw.ticker, iconPath:nw.iconPath, 
                amountCrypto:networkBalanceString}
            // add adjusted balance to balances return object
            balances.push(newBalanceObj);
        }
    });
    return balances;
}

Note: The array is a different reference, so there should be no issue with shallow equality checks. Also, the updated balances array contains objects, but the length is logged as zero as shown in the first code snippet. Any help will be much apreciated!

CodePudding user response:

You are mutating balances instead of updating it.

Change balances.push to setBalances(prevState => [...prevState, newBalance])

CodePudding user response:

Issue

The issue is that you are iterating the networksFromDb array in a forEach loop with an asynchronous callback. The asynchronous callback ins't the issue, it is that Array.protptype.forEach is synchronous, the the getBalanceAllNetworks callback can't wait for the loop callbacks to resolve. It returns the empty balances array to the caller before the array is populate.

The array is still populated however, and the clicking the link is enough to trigger a React rerender and expose the mutated balances state array.

Solution

Instead of using a .forEach loop for the asynchronous callback, map networksFromDb to an array of Promises and use Promise.all and wait for them all to resolve before returning the populated balances array.

Example:

const getBalanceAllNetworks = async (
  walletUser: IWallet
): Promise<IBalance[]> => {
  const networksFromDb = this.getSupportedNetworkDbs();

  const asyncCallbacks = networksFromDb
    .filter((nw) => {
      const network: Network = new Network(nw.fullName, nw.ticker);
      return network.getNetworkfamily() == NetworkFamily.EVM;
    })
    .map(async (nw) => {
      const kryptikProvider: KryptikProvider = await this.getKryptikProviderForNetworkDb(
        nw
      );

      if (!kryptikProvider.ethProvider) {
        throw Error(`No ethereum provider set up for ${network.fullName}.`);
      }

      const ethNetworkProvider: JsonRpcProvider = kryptikProvider.ethProvider;

      // gets all addresses for network
      const allAddys: string[] = await walletUser.seedLoop.getAddresses(
        network
      );

      // gets first address for network
      const firstAddy: string = allAddys[0];

      // get provider for network
      const networkBalance = await ethNetworkProvider.getBalance(firstAddy);

      // prettify ether balance
      const networkBalanceAdjusted: Number =
        BigNumber.from(networkBalance)
          .div(BigNumber.from("10000000000000000"))
          .toNumber() / 100;
      const networkBalanceString = networkBalanceAdjusted.toString();

      const newBalanceObj: IBalance = {
        fullName: nw.fullName,
        ticker: nw.ticker,
        iconPath: nw.iconPath,
        amountCrypto: networkBalanceString
      };
      // add adjusted balance to balances return object
      return newBalanceObj;
    });

  const balances: IBalance[] = await Promise.all(asyncCallbacks);
  return balances;
};
  • Related