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

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.