BootNodeDev / dAppBooster

A modern blockchain boilerplate built to quickly get you started with your next project.
https://dappbooster.dev
MIT License
5 stars 1 forks source link

HoC to hooks #303

Open fernandomg opened 2 months ago

fernandomg commented 2 months ago

I was going through the codebase and thought we might change the HoCs to hooks. I believe the suspense wrappers need to stay as HoC/wrappers, as their usage may require a lot of boilerplate. But in the case of the walletStatusVerifier, this is what I got after a long debate (????) with ChatGPT.

[!NOTE] There are ELI5 parts, but I left them as I like how everything is justified and detailed.


Overview

Goal: Replace the HoC and the WalletStatusVerifier component with a custom hook. Benefit: Simplify the code, make it more readable, and embrace the React Hooks pattern. Approach: Create a useWalletStatusVerifier hook that encapsulates the logic previously handled by the HoC and the component.


Step-by-Step Refactoring

1. Analyze the Existing Components

WalletStatusVerifier Component:

2. Create a Custom Hook

We'll create a useWalletStatusVerifier hook that:

Custom Hook Implementation:

import { ReactElement } from 'react'
import { extractChain } from 'viem'

import { Button } from '@/src/components/sharedComponents/ui/Buttons'
import { useWeb3Status } from '@/src/hooks/useWeb3Status'
import { chains, ChainsIds } from '@/src/lib/networks.config'
import { ConnectWalletButton } from '@/src/providers/Web3Provider'

interface WalletStatusVerifierProps {
  chainId?: ChainsIds
  fallback?: ReactElement
  labelSwitchChain?: string
}

export const useWalletStatusVerifier = ({
  chainId,
  fallback = <ConnectWalletButton />,
  labelSwitchChain = 'Switch to',
}: WalletStatusVerifierProps = {}) => {
  const {
    appChainId,
    isWalletConnected,
    isWalletSynced,
    switchChain,
    walletChainId,
  } = useWeb3Status()

  const chainToSwitch = extractChain({
    chains,
    id: chainId || appChainId || chains[0].id,
  })

  if (!isWalletConnected) {
    return { isReady: false, statusElement: fallback }
  }

  if (!isWalletSynced || walletChainId !== chainToSwitch.id) {
    return {
      isReady: false,
      statusElement: (
        <Button onClick={() => switchChain(chainToSwitch.id)}>
          {labelSwitchChain} {chainToSwitch?.name}
        </Button>
      ),
    }
  }

  return { isReady: true, statusElement: null }
}

Explanation: Inputs:

3. Update Components to Use the Custom Hook

Example Usage in a Component:

import { FC } from 'react'
import { useWalletStatusVerifier } from '@/src/hooks/useWalletStatusVerifier'

const MyComponent: FC = () => {
  const { isReady, statusElement } = useWalletStatusVerifier()

  if (!isReady) {
    return statusElement
  }

  // The wallet is connected and synced; render the component's content.
  return (
    <div>
      {/* Your component logic goes here */}
      <p>The wallet is connected and synced!</p>
    </div>
  )
}

Explanation:

Benefits:

  1. Remove the HoC and Component Since we have replaced the functionality with a custom hook, you can remove the withWalletStatusVerifier HoC and the WalletStatusVerifier component from your codebase.

Deleted Code:

// Remove the following from your code:

const WalletStatusVerifier: FC<WalletStatusVerifierProps> = ({ ... }) => { ... }

const withWalletStatusVerifier = <P extends object>(WrappedComponent: ComponentType<P>, ...) => { ... }

Final Code

Custom Hook (useWalletStatusVerifier.ts):

import { ReactElement } from 'react'
import { extractChain } from 'viem'

import { Button } from '@/src/components/sharedComponents/ui/Buttons'
import { useWeb3Status } from '@/src/hooks/useWeb3Status'
import { chains, ChainsIds } from '@/src/lib/networks.config'
import { ConnectWalletButton } from '@/src/providers/Web3Provider'

interface WalletStatusVerifierProps {
  chainId?: ChainsIds
  fallback?: ReactElement
  labelSwitchChain?: string
}

export const useWalletStatusVerifier = ({
  chainId,
  fallback = <ConnectWalletButton />,
  labelSwitchChain = 'Switch to',
}: WalletStatusVerifierProps = {}) => {
  const {
    appChainId,
    isWalletConnected,
    isWalletSynced,
    switchChain,
    walletChainId,
  } = useWeb3Status()

  const chainToSwitch = extractChain({
    chains,
    id: chainId || appChainId || chains[0].id,
  })

  if (!isWalletConnected) {
    return { isReady: false, statusElement: fallback }
  }

  if (!isWalletSynced || walletChainId !== chainToSwitch.id) {
    return {
      isReady: false,
      statusElement: (
        <Button onClick={() => switchChain(chainToSwitch.id)}>
          {labelSwitchChain} {chainToSwitch?.name}
        </Button>
      ),
    }
  }

  return { isReady: true, statusElement: null }
}

Component Usage Example (MyComponent.tsx):

import { FC } from 'react'
import { useWalletStatusVerifier } from '@/src/hooks/useWalletStatusVerifier'

const MyComponent: FC = () => {
  const { isReady, statusElement } = useWalletStatusVerifier({
    chainId: 1, // Optional: specify the chain ID
    fallback: <div>Please connect your wallet</div>, // Optional: custom fallback
    labelSwitchChain: 'Switch Network', // Optional: custom label
  })

  if (!isReady) {
    return statusElement
  }

  return (
    <div>
      {/* Your component logic */}
      <p>Wallet is connected and on the correct network.</p>
    </div>
  )
}

Explanation and Benefits

Why Use a Custom Hook?

Advantages Over HoCs


Additional Customization

You can further customize the hook or its usage:

  1. Customizing the Fallback Component When calling the hook, you can pass a custom fallback component:
const { isReady, statusElement } = useWalletStatusVerifier({
  fallback: <MyCustomConnectWalletButton />,
})
  1. Specifying a Different Chain ID If you need to enforce a specific chain ID:
const { isReady, statusElement } = useWalletStatusVerifier({
  chainId: 137, // For Polygon Mainnet, for example
})
  1. Changing the Switch Chain Button Label Customize the label:
const { isReady, statusElement } = useWalletStatusVerifier({
  labelSwitchChain: 'Please switch to',
})

Handling Edge Cases

If you need to perform additional actions when the wallet becomes ready, you can use useEffect:

import { useEffect } from 'react'

const MyComponent: FC = () => {
  const { isReady, statusElement } = useWalletStatusVerifier()

  useEffect(() => {
    if (isReady) {
      // Perform any additional setup or data fetching
    }
  }, [isReady])

  if (!isReady) {
    return statusElement
  }

  return (
    <div>
      {/* Component content */}
    </div>
  )
}

Conclusion

By refactoring your HoC and component into a custom hook, you achieve the following:


Summary