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
- Use the
useDeployedContractInfo hook in multiple React components simultaneously,
- passing the same contract name and chainId as parameters.
-
- Refresh the app to reset all states.
-
- Observe that some components receive the contract data correctly (status DEPLOYED),
- while others get
undefined because their status is still LOADING or NOT_FOUND.
-
- 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,
};
}
Is there an existing issue for this?
Which method was used to setup Scaffold-Alchemy?
npx create-web3-dapp@latest
Current Behavior
useDeployedContractInfowith the same contractstatusstate toLOADING.statusvalues may become inconsistent between components:DEPLOYEDwhile others remain inLOADINGorNOT_FOUND.undefinedfor the contract dataExpected Behavior
Steps To Reproduce
useDeployedContractInfohook in multiple React components simultaneously,undefinedbecause their status is still LOADING or NOT_FOUND.Anything else?
contractStatusCache) that maps each contract and networkimport { useEffect, useState } from "react";
import { useIsMounted } from "usehooks-ts";
import { usePublicClient } from "wagmi";
import { useSelectedNetwork } from "
/hooks/scaffold-alchemy";/utils/scaffold-alchemy/contract";import {
Contract,
ContractCodeStatus,
ContractName,
UseDeployedContractConfig,
contracts,
} from "
type DeployedContractData = {
data: Contract | undefined;
isLoading: boolean;
};
/**
useDeployedContractInfodans plusieurs composantsstatusinitialisé àLOADING.DEPLOYEDalorsLOADINGouNOT_FOUND. Le résultat était donc que dans certainsdeployedContractDataétaitundefinedalors qu’il aurait dû être défini.contractStatusCache) a été introduit.*/
// 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;
/**
*/
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
useDeployedContractInfowith 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;
}, [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,
};
}