Per-block reactivity
Chain state changes every block. DappQL can keep every useContextQuery / useQuery in your app in sync automatically, opt in with one flag, and reads refetch as new blocks land.
Turning it on
import { DappQLProvider } from '@dappql/react'
<DappQLProvider watchBlocks>
{children}
</DappQLProvider>That's it. Every query that doesn't opt out refetches on every new block. No per-component subscription wiring.
How it works
Under the hood:
- The provider subscribes to wagmi's block-number watcher once, at the root.
- On every new block, the internal query manager triggers a refetch across every active
useContextQueryanduseQueryhook, batched into one multicall. - Data flows back to components; React re-renders what actually changed.
One block → one multicall → every view updated. No fan-out, no per-hook polling, no cascading re-renders.
Tuning the refetch interval
If per-block is too aggressive, refetch every N blocks:
<DappQLProvider watchBlocks blocksRefetchInterval={5}>
{/* ~10s on Ethereum, ~10s on Base, ~1s on Arbitrum */}
</DappQLProvider>blocksRefetchInterval defaults to 1 (every block). Set it to 10 on a fast chain (Arbitrum, Base) if you don't need sub-2s freshness.
Per-query opt-out
For queries where chain freshness doesn't matter, chain-static values like decimals, name, symbol, owner on immutable contracts, flag them isStatic:
const { data } = useQuery(
{ symbol: Token.call.symbol(), decimals: Token.call.decimals() },
{ isStatic: true },
)Static queries fetch once and stay. They don't participate in block-driven refetches, which saves RPC calls without any correctness cost.
For useContextQuery, there's no per-query isStatic, by design, since the whole point of context batching is grouping. If you need a mix of static and live reads, split them: static ones via useQuery({ ... }, { isStatic: true }), live ones via useContextQuery({ ... }).
Per-query custom interval
Need faster refresh on a specific hook? Override blocksRefetchInterval per useQuery:
const { data: price } = useQuery(
{ price: Oracle.call.getPrice(asset) },
{ blocksRefetchInterval: 1 },
)
const { data: slowMetrics } = useQuery(
{ tvl: Registry.call.getTVL() },
{ blocksRefetchInterval: 100 }, // every ~3.3min on Base
)Pausing
Need to disable refetch temporarily (a modal is open, a form is being edited, etc.)? Use paused:
const { data } = useQuery(
{ balance: Token.call.balanceOf(account) },
{ paused: isEditing },
)Paused queries don't fetch at all, no initial load, no refetch. Toggle paused: false to resume. Paired with defaultTo() this gives you a stable fallback for the paused window.
Pinning to a block
Historical queries bypass the watcher entirely, they resolve once at the specified block and don't refetch:
const { data } = useQuery(
{ supply: Token.call.totalSupply() },
{ blockNumber: 44_500_000n },
)Useful for TWAP calculations, "as of last snapshot" dashboards, or auditing.
Performance notes
- One multicall per refetch cycle. Scales linearly with contract count, not hook count. A page with 100
useContextQuerycalls pulling 300 reads total → 1 RPC per block, not 300. notifyOnChangeProps: ['data', 'error']. DappQL tells TanStack Query to only re-render when these change.isFetchingtoggling between refetches doesn't re-render the tree.- Default
batchSizeis 1024. If your batch exceeds this, multicalls chunk automatically. - RPC rate limits apply. Alchemy/Infura/QuickNode free tiers typically handle per-block refetch without throttling for moderate traffic. Paid tiers lift the ceiling further.
When to turn off watchBlocks
- Server-rendered pages that don't need live updates. Use
@dappql/async'squeryfor SSR data,watchBlocksfor hydrated client state (or skip it if stale-on-load is fine). - Heavily static views (about pages, deploy addresses, immutable token metadata), if the whole view is static, leave
watchBlocksoff at the provider and let wagmi handle any dynamic state you still need. - RPC-budget-conscious deployments, per-block refetch burns RPC credits. Increase
blocksRefetchIntervalor turn off globally, with per-querywatchBlocks: trueoverrides on the few reads that need live freshness.
Interaction with mutations
Mutations do NOT trigger per-block refetches, they trigger a refetch after the transaction confirms. If you need a read to refresh on a mutation, it'll happen automatically via the next block-driven refetch with watchBlocks on, or you can call refetch() manually from the query result.
Related
- Provider setup,
watchBlocks,blocksRefetchIntervaloptions. useContextQuery, the primary reactive hook.useQuery, per-query options (isStatic,paused,blockNumber).