The built-in hooks (useView, useBalance, etc.) are intentionally thin - they manage loading and error states but don’t include caching, deduplication, or background refetching.
For production applications, we recommend pairing @near-kit/react with a data fetching library:
React Query Most popular. Full-featured with devtools, mutations, and infinite queries.
SWR Lightweight alternative from Vercel. Simpler API, smaller bundle.
React Query
TanStack Query (React Query) is the most popular data fetching library for React. It provides caching, background updates, stale-while-revalidate, and excellent DevTools.
Installation
npm install @tanstack/react-query
Setup
Add QueryClientProvider alongside NearProvider
import { QueryClient , QueryClientProvider } from "@tanstack/react-query"
import { Near } from "near-kit"
import { NearProvider } from "@near-kit/react"
const queryClient = new QueryClient ()
const near = new Near ({ network: "mainnet" })
function App () {
return (
< QueryClientProvider client = { queryClient } >
< NearProvider near = { near } >
< YourApp />
</ NearProvider >
</ QueryClientProvider >
)
}
Create custom hooks using useNear
import { useQuery , useMutation , useQueryClient } from "@tanstack/react-query"
import { useNear } from "@near-kit/react"
// Cached view call
export function useMessages ( limit : number ) {
const near = useNear ()
return useQuery ({
queryKey: [ "messages" , limit ],
queryFn : () => near . view < Message []>( "guestbook.near" , "get_messages" , { limit }),
staleTime: 30_000 , // Consider fresh for 30 seconds
})
}
// Mutation with cache invalidation
export function useAddMessage () {
const near = useNear ()
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : ( text : string ) =>
near . call ( "guestbook.near" , "add_message" , { text }),
onSuccess : () => {
// Invalidate messages cache after adding
queryClient . invalidateQueries ({ queryKey: [ "messages" ] })
},
})
}
Complete Example
Full Guestbook with React Query
import { useQuery , useMutation , useQueryClient } from "@tanstack/react-query"
import { useNear } from "@near-kit/react"
interface Message {
id : number
sender : string
text : string
}
// Custom hooks
function useMessages () {
const near = useNear ()
return useQuery ({
queryKey: [ "guestbook" , "messages" ],
queryFn : () => near . view < Message []>( "guestbook.near" , "get_messages" , { limit: 50 }),
staleTime: 10_000 ,
refetchInterval: 30_000 , // Poll every 30s
})
}
function useAddMessage () {
const near = useNear ()
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : async ( text : string ) => {
await near . call ( "guestbook.near" , "add_message" , { text })
},
onSuccess : () => {
queryClient . invalidateQueries ({ queryKey: [ "guestbook" , "messages" ] })
},
})
}
// Components
function MessageList () {
const { data : messages , isLoading , error } = useMessages ()
if ( isLoading ) return < p > Loading messages... </ p >
if ( error ) return < p > Error loading messages </ p >
return (
< ul >
{ messages ?. map (( msg ) => (
< li key = { msg . id } >
< strong > { msg . sender } </ strong > : { msg . text }
</ li >
)) }
</ ul >
)
}
function AddMessageForm () {
const { mutate , isPending } = useAddMessage ()
const [ text , setText ] = useState ( "" )
const handleSubmit = ( e : FormEvent ) => {
e . preventDefault ()
mutate ( text , {
onSuccess : () => setText ( "" ),
})
}
return (
< form onSubmit = { handleSubmit } >
< input
value = { text }
onChange = { ( e ) => setText ( e . target . value ) }
placeholder = "Your message"
disabled = { isPending }
/>
< button type = "submit" disabled = { isPending || ! text } >
{ isPending ? "Sending..." : "Send" }
</ button >
</ form >
)
}
function Guestbook () {
return (
< div >
< h1 > Guestbook </ h1 >
< AddMessageForm />
< MessageList />
</ div >
)
}
Patterns
Optimistic Updates
Dependent Queries
Parallel Queries
Update the UI immediately, then reconcile with the server response: function useAddMessage () {
const near = useNear ()
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : ( text : string ) =>
near . call ( "guestbook.near" , "add_message" , { text }),
// Optimistically add the message
onMutate : async ( text ) => {
await queryClient . cancelQueries ({ queryKey: [ "messages" ] })
const previous = queryClient . getQueryData < Message []>([ "messages" ])
queryClient . setQueryData < Message []>([ "messages" ], ( old ) => [
... ( old ?? []),
{ id: Date . now (), sender: "you.near" , text },
])
return { previous }
},
// Rollback on error
onError : ( err , text , context ) => {
queryClient . setQueryData ([ "messages" ], context ?. previous )
},
// Refetch to get the real data
onSettled : () => {
queryClient . invalidateQueries ({ queryKey: [ "messages" ] })
},
})
}
Fetch data that depends on other data: function useUserProfile ( accountId : string ) {
const near = useNear ()
// First, check if account exists
const { data : exists } = useQuery ({
queryKey: [ "account-exists" , accountId ],
queryFn : () => near . accountExists ( accountId ),
})
// Then fetch profile only if account exists
return useQuery ({
queryKey: [ "profile" , accountId ],
queryFn : () => near . view ( "profiles.near" , "get_profile" , { account_id: accountId }),
enabled: exists === true , // Only runs if account exists
})
}
Fetch multiple resources in parallel: function useDashboardData ( accountId : string ) {
const near = useNear ()
const results = useQueries ({
queries: [
{
queryKey: [ "balance" , accountId ],
queryFn : () => near . getBalance ( accountId ),
},
{
queryKey: [ "nfts" , accountId ],
queryFn : () => near . view ( "nft.near" , "nft_tokens_for_owner" , { account_id: accountId }),
},
{
queryKey: [ "staking" , accountId ],
queryFn : () => near . view ( "poolv1.near" , "get_account" , { account_id: accountId }),
},
],
})
return {
balance: results [ 0 ]. data ,
nfts: results [ 1 ]. data ,
staking: results [ 2 ]. data ,
isLoading: results . some (( r ) => r . isLoading ),
}
}
SWR
SWR is a lightweight data fetching library from Vercel. It’s simpler than React Query with a smaller bundle size.
Installation
Setup
Add SWRConfig alongside NearProvider
import { SWRConfig } from "swr"
import { Near } from "near-kit"
import { NearProvider } from "@near-kit/react"
const near = new Near ({ network: "mainnet" })
function App () {
return (
< SWRConfig value = { { revalidateOnFocus: false } } >
< NearProvider near = { near } >
< YourApp />
</ NearProvider >
</ SWRConfig >
)
}
Create custom hooks using useNear
import useSWR from "swr"
import useSWRMutation from "swr/mutation"
import { useNear } from "@near-kit/react"
// Cached view call
export function useMessages ( limit : number ) {
const near = useNear ()
return useSWR (
[ "messages" , limit ],
() => near . view < Message []>( "guestbook.near" , "get_messages" , { limit })
)
}
// Mutation
export function useAddMessage () {
const near = useNear ()
return useSWRMutation (
"messages" ,
( _ , { arg : text } : { arg : string }) =>
near . call ( "guestbook.near" , "add_message" , { text })
)
}
Complete Example
import useSWR , { mutate } from "swr"
import useSWRMutation from "swr/mutation"
import { useNear } from "@near-kit/react"
interface Message {
id : number
sender : string
text : string
}
// Custom hooks
function useMessages () {
const near = useNear ()
return useSWR ( "messages" , () =>
near . view < Message []>( "guestbook.near" , "get_messages" , { limit: 50 })
)
}
function useAddMessage () {
const near = useNear ()
return useSWRMutation (
"messages" ,
async ( _ , { arg : text } : { arg : string }) => {
await near . call ( "guestbook.near" , "add_message" , { text })
},
{
onSuccess : () => mutate ( "messages" ), // Revalidate after success
}
)
}
// Components
function MessageList () {
const { data : messages , isLoading , error } = useMessages ()
if ( isLoading ) return < p > Loading messages... </ p >
if ( error ) return < p > Error loading messages </ p >
return (
< ul >
{ messages ?. map (( msg ) => (
< li key = { msg . id } >
< strong > { msg . sender } </ strong > : { msg . text }
</ li >
)) }
</ ul >
)
}
function AddMessageForm () {
const { trigger , isMutating } = useAddMessage ()
const [ text , setText ] = useState ( "" )
const handleSubmit = async ( e : FormEvent ) => {
e . preventDefault ()
await trigger ( text )
setText ( "" )
}
return (
< form onSubmit = { handleSubmit } >
< input
value = { text }
onChange = { ( e ) => setText ( e . target . value ) }
placeholder = "Your message"
disabled = { isMutating }
/>
< button type = "submit" disabled = { isMutating || ! text } >
{ isMutating ? "Sending..." : "Send" }
</ button >
</ form >
)
}
Patterns
Conditional Fetching
Revalidation
Global Mutate
function useProfile ( accountId ?: string ) {
const near = useNear ()
return useSWR (
accountId ? [ "profile" , accountId ] : null , // null key = don't fetch
() => near . view ( "profiles.near" , "get_profile" , { account_id: accountId })
)
}
function useBalance ( accountId : string ) {
const near = useNear ()
return useSWR (
[ "balance" , accountId ],
() => near . getBalance ( accountId ),
{
refreshInterval: 10_000 , // Refetch every 10s
revalidateOnFocus: true , // Refetch when window regains focus
dedupingInterval: 2_000 , // Dedupe requests within 2s
}
)
}
import { mutate } from "swr"
// Revalidate specific key
mutate ([ "balance" , "alice.near" ])
// Revalidate all keys starting with "balance"
mutate (( key ) => Array . isArray ( key ) && key [ 0 ] === "balance" )
// Revalidate everything
mutate (() => true )
Comparison
Feature Built-in Hooks React Query SWR Bundle size 0 KB ~13 KB ~4 KB Caching ❌ ✅ ✅ Deduplication ❌ ✅ ✅ Background refetch ❌ ✅ ✅ Stale-while-revalidate ❌ ✅ ✅ Optimistic updates ❌ ✅ ✅ DevTools ❌ ✅ ❌ Infinite queries ❌ ✅ ✅ Learning curve Low Medium Low
Recommendation : Use built-in hooks for prototypes and simple apps. For production, choose React Query for complex apps with mutations, or SWR for simpler read-heavy apps.
Common Pitfalls
Data not refreshing after mutations
Problem : You call useCall to submit a transaction, then call refetch() from useView, but the data doesn’t update.
Cause : React state updates are batched and asynchronous. When you call refetch() immediately after a mutation, React may not have re-rendered yet, so the refetch uses stale parameters.
// ❌ This won't work reliably
const { refetch : refetchMessages } = useView ({ ... })
const { mutate } = useCall ({ ... })
const handleSubmit = async () => {
await mutate ({ text: "Hello" })
await refetchMessages () // May use stale args!
}
Solution : Use React Query’s invalidateQueries() which properly handles the timing:
// ✅ This works correctly
const { mutate } = useMutation ({
mutationFn : ( text ) => near . call ( "contract" , "add_message" , { text }),
onSuccess : () => {
queryClient . invalidateQueries ({ queryKey: [ "messages" ] })
},
})
Queries not refetching when args change
Problem : You change the arguments to useView but it doesn’t refetch.
Cause : JavaScript object identity. If you create a new object each render, useView sees it as the same value (via JSON serialization), but if you’re computing args from state that hasn’t updated yet, you get stale data.
Solution : Ensure args are derived from state that has actually updated, or use React Query which handles this more predictably.