兩個 hook 的資料可能不一致
const { data: usdcBalance1 } = useReadContracts({
contracts: [{
address: usdcAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [accountAddress]
}]
});
const { data: usdcBalance2 } = useReadContract({
address: usdcAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [accountAddress]
});
function useReadContracts(parameters) {
return useQuery({
queryKey: readContractsQueryKey(parameters),
queryFn: () => {
return multicall({
contracts: parameters.contracts,
// ...other options
});
},
});
}
function useReadContract(parameters) {
return useQuery({
queryKey: readContractQueryKey(parameters),
queryFn: () => {
return readContract({
address: parameters.address,
abi: parameters.abi,
functionName: parameters.functionName,
args: parameters.args,
// ...other options
});
},
});
}
整組 contracts 只用一個 query key
沒有針對每個子 contract call
設定各自的 query key
function useReadContractsEnhanced(parameters) {
return useQueries({
queries: parameters.map((param) => ({
queryKey: readContractQueryKey(param),
queryFn: () => {
return readContract(param);
},
})),
});
}
但是這樣
就沒有 batch rpc requests 了
這樣資料就會跟 useReadContract 的同步了
batshit 的 Batcher
batchLoadFn
DataLoader 一定不會一被 load 就直接執行 batchLoadFn
因為這樣不同地方 load 的就不會一起執行了
可以指定 batchLoadFn 什麼時候執行
或是它預設會在 next tick 執行
在 browser 則是 setTimeout(callback)
const loader = new DataLoader(
async (contracts) => {
return multicall({ contracts });
}
);
function useReadContractsEnhanced(parameters) {
return useQueries({
queries: parameters.map((param) => ({
queryKey: readContractQueryKey(param),
queryFn: () => {
return loader.load(param);
},
})),
});
}
就仍然可以 multicall
const ListComponent = () => {
const items = useListItems();
const { data: dataA } = useBatchGetItemData1(items);
const { data: dataB } = useBatchGetItemData2(items);
const { data: dataC } = useBatchGetItemData3(items);
return (
<div>
{items.map((item, index) => (
<ItemComponent
key={item.id}
dataA={dataA[index]}
dataB={dataB[index]}
dataC={dataC[index]}
/>
))}
</div>
);
};
const ItemComponent = ({ dataA, dataB, dataC }) => {
return (
<ul>
<li>{dataA}</li>
<li>{dataB}</li>
<li>{dataC}</li>
</ul>
);
};
const ListComponent = () => {
const items = useListItems();
return (
<div>
{items.map((item, index) => (
<ItemComponent key={item.id} item={item} />
))}
</div>
);
};
const ItemComponent = ({ item }) => {
const { data: dataA } = useGetItemDataA(item);
const { data: dataB } = useGetItemDataB(item);
const { data: dataC } = useGetItemDataC(item);
return (
<ul>
<li>{dataA}</li>
<li>{dataB}</li>
<li>{dataC}</li>
</ul>
);
};
就可以自由選擇要用哪一種寫法,效能會是一樣的
(React 的話甚至可能在 ItemComponent 拿資料更好)
function useReadContractEnhanced(parameters) {
return useQuery({
queryKey: readContractQueryKey(parameters),
queryFn: () => {
return loader.load(parameters);
},
});
}
這樣只要同時被 trigger 的 query
就都會一起 multicall
其實可以減少一些為了效能考量而使用的 useReadContracts
因為使用 useReadContract 一樣可一起 multicall
// getReadContractDataLoader.ts
import DataLoader from 'dataloader';
import type { BlockTag, ContractFunctionParameters, StateOverride } from 'viem';
import type { Config } from 'wagmi';
import type { ReadContractParameters } from 'wagmi/actions';
import { multicall } from 'wagmi/actions';
import { hashFn } from 'wagmi/query';
type ReadContractParametersWithChainId = ReadContractParameters & { chainId: number };
type ReadContractDataLoader = DataLoader<ReadContractParametersWithChainId, any>;
type ContractReadContext = [chainId: number, blockNumber?: bigint, blockTag?: BlockTag, stateOverride?: StateOverride];
const ReadContractDataLoaderByConfig = new Map<Config, ReadContractDataLoader>();
export const getReadContractDataLoader = (config: Config) => {
if (ReadContractDataLoaderByConfig.has(config)) {
return ReadContractDataLoaderByConfig.get(config) as ReadContractDataLoader;
}
const loader = new DataLoader(
async (parameters: readonly ReadContractParametersWithChainId[]) => {
const paramsByContext: Record<
string,
{ context: ContractReadContext; contract: ContractFunctionParameters; index: number }[]
> = {};
for (const [index, param] of parameters.entries()) {
const { chainId, blockNumber, blockTag, stateOverride, ...contract } = param;
const context = [chainId, blockNumber, blockTag, stateOverride] satisfies ContractReadContext;
const contextHash = hashFn(context);
if (!paramsByContext[contextHash]) {
paramsByContext[contextHash] = [];
}
paramsByContext[contextHash].push({ context, contract, index });
}
const groupedParams = Object.values(paramsByContext);
const promises = () =>
groupedParams.map(grouped => {
const [chainId, blockNumber, blockTag, stateOverride] = grouped[0].context;
const contracts = grouped.map(({ contract }) => contract);
return multicall(config, {
chainId,
blockNumber,
blockTag,
stateOverride,
contracts,
});
});
const multicallResults = (await Promise.all(promises())).flat();
const indexes = groupedParams.flatMap(grouped => grouped.map(({ index }) => index));
return multicallResults.reduce((acc, result, index) => {
acc[indexes[index]] = result.error ?? result.result;
return acc;
}, [] as any[]);
},
// We only need the data loader to batch the requests, cache is handled by the query client
{ cache: false }
);
ReadContractDataLoaderByConfig.set(config, loader);
return loader;
};
// useReadContractEnhanced.ts
import { type UseQueryResult, useQuery } from '@tanstack/react-query';
import type { Abi, ContractFunctionArgs, ContractFunctionName, ReadContractParameters } from 'viem';
import { type Config, type UseReadContractParameters, useChainId, useConfig } from 'wagmi';
import { type ReadContractData, type ReadContractOptions, hashFn, readContractQueryKey } from 'wagmi/query';
import { getReadContractDataLoader } from '../libs/utils/getReadContractDataLoader';
export const useReadContractEnhanced = <
const abi extends Abi | readonly unknown[],
functionName extends ContractFunctionName<abi, 'pure' | 'view'>,
args extends ContractFunctionArgs<abi, 'pure' | 'view', functionName>,
config extends Config = Config,
selectData = ReadContractData<abi, functionName, args>
>(
parameters: UseReadContractParameters<abi, functionName, args, config, selectData>
): UseQueryResult<selectData> => {
const config = useConfig(parameters);
const chainId = useChainId({ config });
const loader = getReadContractDataLoader(config);
const { query, config: _, ...contractOptions } = parameters;
return useQuery({
queryKey: readContractQueryKey({ chainId, ...parameters } as ReadContractOptions<abi, functionName, args, config>),
queryKeyHashFn: hashFn,
queryFn: () => loader.load({ chainId, ...(contractOptions as ReadContractParameters) }),
...query,
enabled: Boolean(
contractOptions.address && contractOptions.abi && contractOptions.functionName && (query?.enabled ?? true)
),
});
};
// useReadContractsEnhanced.ts
import { type UseQueryResult, useQueries } from '@tanstack/react-query';
import omit from 'lodash/omit';
import type { Abi, ContractFunctionArgs, ContractFunctionName, ReadContractParameters } from 'viem';
import { type Config, type UseReadContractParameters, useChainId, useConfig } from 'wagmi';
import { type ReadContractData, type ReadContractOptions, hashFn, readContractQueryKey } from 'wagmi/query';
import { getReadContractDataLoader } from '../libs/utils/getReadContractDataLoader';
export const useReadContractsEnhanced = <
const abi extends Abi | readonly unknown[],
functionName extends ContractFunctionName<abi, 'pure' | 'view'>,
args extends ContractFunctionArgs<abi, 'pure' | 'view', functionName>,
config extends Config = Config,
selectData = ReadContractData<abi, functionName, args>
>(
parameters: UseReadContractParameters<abi, functionName, args, config, selectData>[]
): UseQueryResult<selectData>[] => {
const config = useConfig();
const chainId = useChainId({ config });
return useQueries({
queries: parameters.map(param => ({
queryKey: readContractQueryKey({ chainId, ...param } as ReadContractOptions<abi, functionName, args, config>),
queryKeyHashFn: hashFn,
queryFn: async () => {
const loader = getReadContractDataLoader(param.config ?? config);
return loader.load({ chainId, ...(omit(param, 'config', 'query') as ReadContractParameters) });
},
...param.query,
enabled: Boolean(param.abi && param.address && param.functionName && (param.query?.enabled ?? true)),
})),
});
};