Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useWatchBlocks subscribes / unsubscribes on each new Block with WebSocket #4352

Open
1 task done
coderodigital opened this issue Oct 18, 2024 · 5 comments
Open
1 task done
Labels
Good First Issue Misc: Good First Issue

Comments

@coderodigital
Copy link

Check existing issues

Describe the bug

When using useWatchBlocks with a WebSocket connection, the expected behavior is as follows:

  1. Subscribe to new blocks
  2. Receive (onBlock) callbacks without unsubscribing
  3. Unsubscribe when the component using useWatchBlocks is unmounted / modified

The observed behavior is:

  1. Subscribe to new heads
  2. Receive a block, execute onBlock
  3. Unsubscribe as a dependency seems to change in the implementation of useWatchBlocks, probably caused by useEffect in the useWatchBlocks implementation
  4. Re-Subscribe and repeat

It's important to be aware that subscribing / Unsubscribing can be an expensive call on certain API providers (e.g. 10 Compute Units on Alchemy), causing unnecessary spikes in API Usage.

Alchemy Compute Unit Costs

Link to Minimal Reproducible Example

No response

Steps To Reproduce

  1. Configure Wagmi with a WebSocket RPC endpoint
  2. Use the snippets below, which only slightly modify the useWatchBlocks code to get console log outputs
    You will observe that the subscription is closed after each block, and a new subscription is established.

Code to use the modified hook:

  useWatchBlocks({
    emitOnBegin: true,
    chainId: Constants.CHAIN_ID,
    onBlock: (b) => {
      console.log(b)
    }
  })

The modified hook:

'use client'

import {
type Config,
type ResolvedRegister,
type WatchBlocksParameters,
watchBlocks,
} from '@wagmi/core'
import type { UnionCompute, UnionExactPartial } from '@wagmi/core/internal'
import { useEffect } from 'react'
import type { BlockTag } from 'viem'

// Import wagmi config and constants for test
import { config } from '../AppKit'

export type EnabledParameter = {
enabled?: boolean | undefined
}

export type ConfigParameter<config extends Config = Config> = {
config?: Config | config | undefined
}

export type UseWatchBlocksParameters<
includeTransactions extends boolean = false,
blockTag extends BlockTag = 'latest',
config extends Config = Config,
chainId extends
config['chains'][number]['id'] = config['chains'][number]['id'],
> = UnionCompute<
UnionExactPartial<
  WatchBlocksParameters<includeTransactions, blockTag, config, chainId>
> &
ConfigParameter<config> &
EnabledParameter
>

export type UseWatchBlocksReturnType = void

/** https://wagmi.sh/react/hooks/useWatchBlocks */
export function useWatchBlocks<
config extends Config = ResolvedRegister['config'],
chainId extends
config['chains'][number]['id'] = config['chains'][number]['id'],
includeTransactions extends boolean = false,
blockTag extends BlockTag = 'latest',
>(
parameters: UseWatchBlocksParameters<
  includeTransactions,
  blockTag,
  config,
  chainId
> = {} as any,
): UseWatchBlocksReturnType {
const { enabled = true, onBlock, config: _, ...rest } = parameters
const chainId = parameters.chainId

// TODO(react@19): cleanup
// biome-ignore lint/correctness/useExhaustiveDependencies: `rest` changes every render so only including properties in dependency array
useEffect(() => {
  if (!enabled) return
  if (!onBlock) return

  // Slightly modify for console logs
  console.log('WATCH')
  const unwatch = watchBlocks(config, {
    ...(rest as any),
    chainId,
    onBlock,
  })

  return () => {
    console.log('UNWATCH')
    unwatch()
  }
}, [
  chainId,
  config,
  enabled,
  onBlock,
  ///
  rest.blockTag,
  rest.emitMissed,
  rest.emitOnBegin,
  rest.includeTransactions,
  rest.onError,
  rest.poll,
  rest.pollingInterval,
  rest.syncConnectedChain,
])
}

What Wagmi package(s) are you using?

wagmi

Wagmi Package(s) Version(s)

2.12.17

Viem Version

2.21.21

TypeScript Version

5.6.0

Anything else?

No response

@coderodigital
Copy link
Author

After investigating the issue further, it seems that onBlock, when passed like in the official example code:

import { useWatchBlocks } from 'wagmi'

function App() {
  useWatchBlocks({
    onBlock(block) {
      console.log('New block', block.number)
    },
  })
}

is causing the behavior. The following change seems to help:

const onBlock = useCallback((block) => {
  console.log('New block', block.number);
}, []);
  
useWatchBlocks({
  onBlock
})

However, I suggest a code-change where modifying the callback is not causing a subscribe / unsubscribe.

@coderodigital
Copy link
Author

Just a quick draft on how this could be improved. Using a ref to store onBlock to prevent unnecessary triggers in the useEffect.

'use client'

import {
  type Config,
  type ResolvedRegister,
  type WatchBlocksParameters,
  watchBlocks,
} from '@wagmi/core'
import type { UnionCompute, UnionExactPartial } from '@wagmi/core/internal'
import { useEffect, useRef, useState } from 'react'
import type { BlockTag, OnBlockParameter } from 'viem'

// Import wagmi config and constants for test
import { config } from '../AppKit'

export type EnabledParameter = {
  enabled?: boolean | undefined
}

export type ConfigParameter<config extends Config = Config> = {
  config?: Config | config | undefined
}

export type UseWatchBlocksParameters<
  includeTransactions extends boolean = false,
  blockTag extends BlockTag = 'latest',
  config extends Config = Config,
  chainId extends
  config['chains'][number]['id'] = config['chains'][number]['id'],
> = UnionCompute<
  UnionExactPartial<
    WatchBlocksParameters<includeTransactions, blockTag, config, chainId>
  > &
  ConfigParameter<config> &
  EnabledParameter
>

export type UseWatchBlocksReturnType = void

/** https://wagmi.sh/react/hooks/useWatchBlocks */
export function useWatchBlocks<
  config extends Config = ResolvedRegister['config'],
  chainId extends
  config['chains'][number]['id'] = config['chains'][number]['id'],
  includeTransactions extends boolean = false,
  blockTag extends BlockTag = 'latest',
>(
  parameters: UseWatchBlocksParameters<
    includeTransactions,
    blockTag,
    config,
    chainId
  > = {} as any,
): UseWatchBlocksReturnType {
  const { enabled = true, onBlock, config: _, ...rest } = parameters
  const chainId = parameters.chainId

  // Use a ref to hold the onBlock callback
  const onBlockRef = useRef(onBlock)
  const [watchEnabled, setWatchEnabled] = useState(enabled && !!onBlock)

  // Update the ref if onBlock changes, but don't cause re-subscription
  useEffect(() => {
    console.log('UPDATE REF')
    onBlockRef.current = onBlock

    // Adjust watchEnabled when onBlock goes from undefined to defined or vice versa
    if (!!onBlock !== watchEnabled) {
      setWatchEnabled(!!onBlock)
    }
  }, [onBlock, watchEnabled])

  useEffect(() => {
    if (!watchEnabled) return

    console.log('WATCH')
    const unwatch = watchBlocks(config, {
      ...(rest as any),
      chainId,
      // Use the latest onBlock from the ref
      onBlock: (block, prevBlock) => {
        if (onBlockRef.current) {
          onBlockRef.current(block, prevBlock)
        }
      },
    })

    return () => {
      console.log('UNWATCH')
      unwatch()
    }
  }, [
    chainId,
    config,
    watchEnabled, // Dependency that changes based on whether we should be watching or not
    ///
    rest.blockTag,
    rest.emitMissed,
    rest.emitOnBegin,
    rest.includeTransactions,
    rest.onError,
    rest.poll,
    rest.pollingInterval,
    rest.syncConnectedChain,
  ])
}

@tmm tmm added the Good First Issue Misc: Good First Issue label Nov 5, 2024
@1997roylee
Copy link
Contributor

I cant reproduce ur issue, I dont see there're unsubscribe action on new block. Could you provide a reproduce example?

@ga-reth
Copy link

ga-reth commented Dec 15, 2024

@coderodigital unable to repro your issue

Wagmi version - 2.14.1

'use client'

import { createConfig, WagmiProvider, webSocket } from "wagmi";
import { mainnet } from "wagmi/chains";
import { useWatchBlocks } from "./watchblock";

export const config = createConfig({
    chains: [mainnet],
    transports: {
      [mainnet.id]: webSocket('<WS_RPC_URL>'),
    },
  });

function Xyz() {
    useWatchBlocks({
        onBlock: (b) => {
          console.log('found new block >>>>', b.hash.slice(0, 10))
        }
      })

  return <></>
}

export default function WatchBlocks() {
  return (
    <WagmiProvider config={config}>
            <Xyz />
    </WagmiProvider>
  );
}

where useWatchBlocks is your adapted hook - can you share a repro?

@coderodigital
Copy link
Author

Let me prepare an example repo (give me a day). To identify the bug, you must examine the network traffic on the open WebSocket connection. The code works as intended but could be more effective when using the WebSocket subscription (which only subscribes once). I will also share screenshots of the network traffic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Good First Issue Misc: Good First Issue
Projects
None yet
Development

No branches or pull requests

5 participants
@tmm @ga-reth @1997roylee @coderodigital and others