Skip to main content
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

1

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

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

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

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"] })
    },
  })
}

SWR

SWR is a lightweight data fetching library from Vercel. It’s simpler than React Query with a smaller bundle size.

Installation

npm install swr

Setup

1

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

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

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

Comparison

FeatureBuilt-in HooksReact QuerySWR
Bundle size0 KB~13 KB~4 KB
Caching
Deduplication
Background refetch
Stale-while-revalidate
Optimistic updates
DevTools
Infinite queries
Learning curveLowMediumLow
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.