Adding User Transactions and Fighting Async State Updates

Today capped off my first week doing Web3 development full-time, and I have to say I'm having a blast. I've gotten to know some other developers and can feel the energy in the space. There is a ton of interest in hiring for Solidity/web3 development, and I've thought of several personal projects I'd like to build. The only limiting factor at this point is finding the time to do everything.

Additionally, I'm riding a little high this evening after debugging some pesky async state issues (thanks to fellow Solidity Guild member: julioz for providing some expert React advice).

Fixing a Conditional UI Issue

I ended yesterday with an issue in my component structure on the Raider Vault app for trying to expand/collapse each vault row to expose user actions for depositing and withdrawing from the vaults (similar to the v1 Yearn Vault UI). The issue I had was silly in hindsight. I ended up creating state variables for the CSS classNames and just used a display: none tag on the expanded section when it needed to be hidden. With a few other clever border rounding adjustments, I had a fluid, dynamic row component.

Conditional UI view

Adding On-Chain User Actions

Now that the UI was working, I need to make it do something!

I designed my "vaults" state object to include all the data and objects needed to display the vault information and execute transactions. This includes an ethers.Contract instance with the active ethers.Signer connected for both the Vault contract and the Deposit Token contract (needed to approve sending the tokens to the vault). Having the state already setup for each component made the implementation of these functions straightforward.

The deposit function required 2 steps.

  1. Check if the user has approved the vault for an allowance of the depositToken and request one if not. 2. Send the deposit.

Here's the function (removed try/catch and console.log blocks for simplicity):

const deposit = async () => {
    let _depositValue = ethers.utils.parseUnits(depositValue, 'ether');

    // Check if the user has approved the vault for deposit
    const allowance = await vault.depositToken.contract.allowance(user, vault.address);
    if (allowance < depositValue) {
        const approvalTxn = await vault.depositToken.contract.approve(vault.address, _depositValue)
        await approvalTxn.wait();
    }

    // Deposit the tokens in the vault
    const depositTxn = await vault.contract.deposit(_depositValue);
    await depositTxn.wait();
    setDepositValue('0');
    loadingVaults.current = true;
};

The withdraw function is simply calling that function from the vault contract.

const withdraw = async () => {
    let _withdrawValue = ethers.utils.parseUnits(withdrawValue, 'ether');
    const withdrawTxn = await vault.contract.withdraw(_withdrawValue);
    await withdrawTxn.wait();
    setWithdrawValue('0');
    loadingVaults.current = true;
}

Here, I trigger a vault update after each transaction is sent to refresh the data displayed on the page by setting loadingVaults.current = true. loadingVaults is a React Ref created with useRef.

Async State Issues

After getting all of the user functionality working today, I started having weird state loading issues that would cause the vaults to not render on the page. I've built multiple apps in React, but I still struggle with getting the state and component lifecycles in sync on almost every project. After a lot troubleshooting with react-dev-tools and console.log outputs, I finally isolated my issue to the getVaultData function. The gist of the issue was that getVaultData is a synchronous function that set the state for the vaults, but it had a map function from a static object to load each vault. The callback function in this map was async because it was loading on-chain data and the vault objects were being passed before the Promises were resolved. Here are the before and after snippets:

Before

const getVaultData = () => {
    // Create vault contract interfaces 
    let _vaults = [];
    let _vault;

    // Loop through the provided vaultInfo to create vaults
    vaultInfo.map(async info => {
       _vault = await loadVault(info);
       console.log(_vault);
       _vaults.push(_vault);
    });

   // Update the vaults state variable
   setVaults(_vaults);
};

After

const getVaultData = () => {
    // Create vault contract interfaces 
    const loadVaults = async () => {
        let _vaults = [];
        let _vault;
        for (let i = 0; i < vaultInfo.length; i++) {
            _vault = await loadVault(vaultInfo[i]);
            _vaults = [..._vaults, _vault];
        }
        return _vaults;
    };

    loadVaults().then(_vaults => setVaults(_vaults));
};

The real magic here is having the setVaults function call in the loadVaults.then callback function. This ensures that the state isn't updated until the async loadVaults output is resolved.

That's enough React for today. Cheers.