GitHub
Open Github

UseRequest

Data fetching with global in-memory caching, request deduplication, and status tracking.

Installation

pnpm add @hookraft/userequest

Usage

import { useRequest } from "@hookraft/userequest"
const { data, status, error, isLoading, refetch } = useRequest("/api/user", {
  cacheTime: 30000,
  onSuccess: (data) => console.log("Got data:", data),
  onError: (err) => console.error("Failed:", err),
})

Why it exists

When multiple components on the same page need the same data, most apps fire one request per component:

// Navbar.tsx
const { data: user } = useRequest("/api/user") // request 1

// Sidebar.tsx
const { data: user } = useRequest("/api/user") // request 2

// ProfileCard.tsx
const { data: user } = useRequest("/api/user") // request 3

Without deduplication — 3 identical network requests fire at the same time.

With useRequest1 request fires. All 3 components wait for the same Promise and receive the same data when it resolves. The server never sees the duplicates.

Full Example

A dashboard where three completely separate components all need the same user data — only one network request fires.

// hooks/useUser.ts
import { useRequest } from "@hookraft/userequest"

type User = {
  id: string
  name: string
  email: string
  avatar: string
  plan: "free" | "pro"
}

export function useUser() {
  return useRequest<User>("/api/user", {
    cacheTime: 60000, // cache for 60 seconds
    onError: () => console.error("Failed to load user"),
  })
}
// components/Navbar.tsx
import { useUser } from "@/hooks/useUser"

export function Navbar() {
  const { data: user, isLoading } = useUser()

  return (
    <nav>
      {isLoading ? (
        <div className="skeleton w-8 h-8 rounded-full" />
      ) : (
        <img src={user?.avatar} alt={user?.name} />
      )}
    </nav>
  )
}
// components/Sidebar.tsx
import { useUser } from "@/hooks/useUser"

export function Sidebar() {
  const { data: user } = useUser()

  return (
    <aside>
      <p>{user?.name}</p>
      <p>{user?.email}</p>
    </aside>
  )
}
// components/PlanBadge.tsx
import { useUser } from "@/hooks/useUser"

export function PlanBadge() {
  const { data: user } = useUser()

  return (
    <span>
      {user?.plan === "pro" ? "Pro" : "Free"}
    </span>
  )
}

All three components call useUser() which calls useRequest("/api/user"). Only one network request fires. When it resolves, all three components update at the same time.

Manual Mode

By default useRequest fires on mount. Set manual: true to control when it runs.

const { data, isLoading, refetch } = useRequest("/api/report", {
  manual: true, // don't fetch on mount
})

// Only fires when the user clicks the button
<button onClick={refetch} disabled={isLoading}>
  {isLoading ? "Loading..." : "Generate Report"}
</button>

Force Refresh

refetch() always bypasses the cache and fires a fresh request — useful for pull-to-refresh or manual sync buttons.

const { data, refetch } = useRequest("/api/notifications")

<button onClick={refetch}>
  Sync notifications
</button>

Custom Fetcher

By default useRequest uses fetch + res.json(). Pass your own fetcher to use axios, a GraphQL client, or any async function.

import axios from "axios"

const { data } = useRequest("/api/products", {
  fetcher: async (url) => {
    const res = await axios.get(url)
    return res.data
  },
})

Clearing the Cache

Use clear() to wipe the cached data for a key — useful after a mutation that changes the data on the server.

const { data: user, clear } = useRequest("/api/user")

const updateProfile = async (newName: string) => {
  await fetch("/api/user", {
    method: "PATCH",
    body: JSON.stringify({ name: newName }),
  })

  // clear the cache so next render fetches fresh data
  clear()
}

Null Key

Pass null as the key to skip the request entirely — useful for conditional fetching.

const { data } = useRequest(isLoggedIn ? "/api/user" : null)
// request only fires when isLoggedIn is true

Options

PropTypeDefault
fetcher
(key: string) => Promise<T>
-
cacheTime
number
30000
dedupe
boolean
true
manual
boolean
false
onSuccess
(data: T) => void
-
onError
(error: unknown) => void
-
onStatusChange
(status: RequestStatus) => void
-

Returns

PropTypeDefault
data
T | undefined
-
status
RequestStatus
-
error
unknown
-
isLoading
boolean
-
isSuccess
boolean
-
isError
boolean
-
is(status)
(s: RequestStatus) => boolean
-
refetch()
() => Promise<void>
-
clear()
() => void
-