Skip to content

bug: <title> useDeployedContractInfo.ts bug when using multiple instance of useScaffoldWriteContract #33

@nennex27

Description

@nennex27

Is there an existing issue for this?

Which method was used to setup Scaffold-Alchemy?

npx create-web3-dapp@latest

Current Behavior

  • When multiple components call useDeployedContractInfo with the same contract
  • and network parameters, each instance initializes its own status state to LOADING.
  • This causes redundant network requests to fetch the contract bytecode.
  • Additionally, the status values may become inconsistent between components:
  • some might have DEPLOYED while others remain in LOADING or NOT_FOUND.
  • This leads to some components receiving undefined for the contract data
  • even though the contract is actually deployed.

Expected Behavior

  • All instances of the hook with the same parameters should share the same deployment status.
  • The hook should avoid redundant network calls by reusing known deployment status.
  • Consequently, all components should consistently receive the correct deployed contract data
  • without delay or mismatch.

Steps To Reproduce

  1. Use the useDeployedContractInfo hook in multiple React components simultaneously,
  • passing the same contract name and chainId as parameters.
    1. Refresh the app to reset all states.
    1. Observe that some components receive the contract data correctly (status DEPLOYED),
  • while others get undefined because their status is still LOADING or NOT_FOUND.
    1. Notice redundant calls to fetch the contract bytecode in the network tab or logs,
  • indicating repeated network requests for the same contract deployment check.

Anything else?

  • Fix Applied:

  • Introduced a global cache (contractStatusCache) that maps each contract and network
  • combination to its current deployment status.
  • On hook initialization, the cache is checked first:
    • If a cached status exists, it is used to initialize the hook's local state.
    • If not, the hook performs the bytecode check and updates the cache accordingly.
  • This caching mechanism prevents duplicated network calls and ensures all hook
  • consumers see a consistent and up-to-date deployment status.

import { useEffect, useState } from "react";
import { useIsMounted } from "usehooks-ts";
import { usePublicClient } from "wagmi";
import { useSelectedNetwork } from "/hooks/scaffold-alchemy";
import {
Contract,
ContractCodeStatus,
ContractName,
UseDeployedContractConfig,
contracts,
} from "
/utils/scaffold-alchemy/contract";

type DeployedContractData = {
data: Contract | undefined;
isLoading: boolean;
};

/**

  • Problème initial :

  • Lors de l'utilisation de ce hook useDeployedContractInfo dans plusieurs composants
  • avec les mêmes paramètres (même contrat et même réseau), chaque instance du hook
  • maintenait son propre état local status initialisé à LOADING.
  • Cela entraînait plusieurs appels réseau simultanés pour vérifier la présence du contrat,
  • et surtout un comportement incohérent : un composant pouvait afficher DEPLOYED alors
  • qu'un autre était encore à LOADING ou NOT_FOUND. Le résultat était donc que dans certains
  • composants deployedContractData était undefined alors qu’il aurait dû être défini.
  • Correction apportée :

  • Pour résoudre ce problème, un cache global (contractStatusCache) a été introduit.
  • Ce cache stocke le statut de déploiement du contrat par combinaison réseau + contrat.
  • Lorsqu'une instance du hook est appelée, elle consulte d'abord ce cache :
    • Si le statut est déjà présent, elle initialise son état local avec cette valeur
  • et évite ainsi un nouvel appel réseau.
    • Sinon, elle effectue la requête pour récupérer le bytecode et met à jour
  • à la fois son état local et le cache global.
  • Cela garantit que toutes les instances du hook partagent le même statut et évite
  • les appels réseau redondants, assurant ainsi une cohérence des données dans toute l’application.
    */
    // Global cache shared across all hook instances.
    // This prevents multiple network calls for the same contract + network pair.
    const contractStatusCache = new Map<string, ContractCodeStatus>();

export function useDeployedContractInfo(
config: UseDeployedContractConfig,
): DeployedContractData;
/**

  • @deprecated Use object parameter version instead: useDeployedContractInfo({ contractName: "YourContract" })
    */
    export function useDeployedContractInfo(
    contractName: TContractName,
    ): DeployedContractData;

export function useDeployedContractInfo(
configOrName: UseDeployedContractConfig | TContractName,
): DeployedContractData {
const isMounted = useIsMounted();

// Normalize configOrName to an object { contractName, chainId? }
const finalConfig: UseDeployedContractConfig =
typeof configOrName === "string" ? { contractName: configOrName } : (configOrName as any);

useEffect(() => {
if (typeof configOrName === "string") {
console.warn(
"Using useDeployedContractInfo with a string parameter is deprecated. Please use the object parameter version instead.",
);
}
}, [configOrName]);

const { contractName, chainId } = finalConfig;
const selectedNetwork = useSelectedNetwork(chainId);
const deployedContract = contracts?.[selectedNetwork.id]?.[contractName as ContractName] as Contract;
const publicClient = usePublicClient({ chainId: selectedNetwork.id });

// Create a unique cache key for this contract + network combination
const cacheKey = ${selectedNetwork.id}-${contractName};

// Try to get the cached contract deployment status
const cachedStatus = contractStatusCache.get(cacheKey);

// Initialize React state with cached status if available; otherwise, start with LOADING
const [status, setStatus] = useState(cachedStatus ?? ContractCodeStatus.LOADING);

useEffect(() => {
// Function to check contract deployment by fetching bytecode from the blockchain
const checkContractDeployment = async () => {
if (!isMounted() || !publicClient) return;

  // If contract instance is not found in local contracts list, mark as NOT_FOUND
  if (!deployedContract) {
    setStatus(ContractCodeStatus.NOT_FOUND);
    contractStatusCache.set(cacheKey, ContractCodeStatus.NOT_FOUND);
    return;
  }

  try {
    // Fetch the contract bytecode at the deployed address
    const code = await publicClient.getBytecode({
      address: deployedContract.address,
    });

    // Determine status based on whether bytecode is empty or not
    const newStatus = code && code !== "0x" ? ContractCodeStatus.DEPLOYED : ContractCodeStatus.NOT_FOUND;

    // Update state and cache with new status
    setStatus(newStatus);
    contractStatusCache.set(cacheKey, newStatus);
  } catch (e) {
    console.error(e);
    // On error, consider contract as not found and update cache/state accordingly
    setStatus(ContractCodeStatus.NOT_FOUND);
    contractStatusCache.set(cacheKey, ContractCodeStatus.NOT_FOUND);
  }
};

// Only perform the deployment check if we don't already have cached status
if (cachedStatus === undefined) {
  checkContractDeployment();
}
// Dependencies include cacheKey so the effect re-runs when contract or network changes

}, [isMounted, cacheKey, deployedContract, publicClient, cachedStatus]);

return {
// Return the deployed contract if status is DEPLOYED, otherwise undefined
data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined,
// Loading state is true only while status is LOADING
isLoading: status === ContractCodeStatus.LOADING,
};
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions