useReadContract(s)

小改造

Wagmi 的

useReadContract 和

useReadContracts   

很好用

但是 useReadContracts 抓來的資料

跟 useReadContract 抓來的

不會自動同步

例如

兩個 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

如果想要 useReadContracts

針對每個子 contract call

可以有自己的 query key

我們可以改用 useQueries

像是這樣

function useReadContractsEnhanced(parameters) {
  return useQueries({
    queries: parameters.map((param) => ({
      queryKey: readContractQueryKey(param),
      queryFn: () => {
        return readContract(param);
      },
    })),
  });
}

但是這樣

就沒有 batch rpc requests 了

這樣資料就會跟 useReadContract 的同步了

解法一

batshit 的 Batcher

解法二

什麼是 DataLoader

batchLoadFn

DataLoader 什麼時候執行 batchLoadFn

DataLoader 一定不會一被 load 就直接執行 batchLoadFn

因為這樣不同地方 load 的就不會一起執行了

可以指定 batchLoadFn 什麼時候執行

或是它預設會在 next tick 執行
在 browser 則是 setTimeout(callback)

在 queryFn 裡面用 DataLoader

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

其實使用 DataLoader 有另一個更大的好處

「效能」vs「關注點分離」

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>
  );
};

如果使用 DataLoader

就可以自由選擇要用哪一種寫法,效能會是一樣的

(React 的話甚至可能在 ItemComponent 拿資料更好)

所以我們也可以為 useReadContract 寫一個

利用 DataLoader 的版本

function useReadContractEnhanced(parameters) {
  return useQuery({
    queryKey: readContractQueryKey(parameters),
    queryFn: () => {
      return loader.load(parameters);
    },
  });
}

這樣只要同時被 trigger 的 query

就都會一起 multicall

使用 DataLoader 後

其實可以減少一些為了效能考量而使用的 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)),
    })),
  });
};
Made with Slides.com