Skip to content
This repository was archived by the owner on May 26, 2026. It is now read-only.
This repository was archived by the owner on May 26, 2026. It is now read-only.

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

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions