Handling State and Fighting with Conditionals

I spent another day working on the UI for my Raider Vault contracts. Most of the work today was boring old wrestling with React versus Web3-specific technologies. My initial observations are that most smart contracts are 80% design/architecting a good solution and 20% implementation, but building the app to expose it to users is the opposite.

After mocking up the UI and handling the connection to the MetaMask Ethereum provider yesterday, I went to work on creating the vault app state, loading data from on-chain, and wiring it up to the vaults UI.

Creating Vault State and Loading On-chain Data

The purpose of the Raider Vault app is to display information about the vaults and allow the user to deposit/withdraw the deposit tokens from the vaults. The vault information to be displayed includes the deposit token, total assets held, the user's balance, additional tokens the user can deposit, applicable fees that the vault charges, and the estimated growth rate. The information required must come from both the vault contracts, the deposit tokens, and the user's wallet.

To get the vault data in the app, I implemented a loadVault function that is called by an overall getVaultData function on each vault hard-coded into a contracts.js file. This is an area where Ethers.js makes your life easier with clean syntax.

// Create contract instances 
const vaultContract = new ethers.Contract(vaultAddress, vaultAbi, signer);
const depositTokenContract = new ethers.Contract(tokenAddress, tokenAbi, signer);
const signerAddress = await signer.getAddress();

// Pull data from contracts to populate the UI
let feePercent = await vaultContract.feePercent(); 
let totalAssets = await vaultContract.totalBalance(); 
let userHoldings = await vaultContract.getUserBalance(signerAddress); 
let availableToDeposit = await depositTokenContract.balanceOf(signerAddress);
let depositTokenName = await depositTokenContract.name();

I originally implemented the state using only useState calls on the highest level components that would use the variable and then passed to lower level components through React props. This is an easy way to get started, but it's the naive approach and quickly becomes difficult to manage. For example, I have an App component at the top-level, multiple layout components, and then a table with vault information after that. To provide the Ethereum state information to the Vault table, that had to be passed and tracked through each intermediate component.

At this point, I decided to pull the state and load scripts out of the components into Context Providers to make this easier to reason about. I had used these before, but Zach Obront pointed me to his DApp Template repo that made this easy to implement (thanks Zach!). Additionally, this simplified the architecture and let me troubleshoot some pesky state lifecycle update issues more easily.

Challenge: Re-rendering conditional components

A big inspiration for my initial UI design was the original Yearn.finance UI. On Yearn, a grid lists out the available vaults and you can click on a "Show" button to expand each row to display the available options.

My initial implementation of this has been buggy, and I'm having issues with tracking the collapse/expanded state since I am dynamically loading the components and using the conditional JSX format to load the right version. Here is a simplified example:

// State variables
const [vaults, ...] = useState([]);
const [vaultsExpanded, ...] = useState({}); // dictionary of address -> bool

// Components
const VaultRow = (props) => {
    return (
        props.isExpanded ? <version1 /> : <version2 />
    ) ;
}

const VaultTable = () => {
    return {vaults.map(vault => <VaultRow isExpanded= {vaultExpanded[address]} vault={vault}))}
}

Tomorrow, I plan to rework this using CSS styles to show/hide the collapsing components instead of trying to force re-rendering of the whole component.