Skip to content

laststance/use-get

Repository files navigation

@laststance/use-get

Tiny React 19 hooks for axios — built on use() + Suspense, with no long-lived cache.

import { useGET } from '@laststance/use-get'

function UserCard({ id }: { id: number }) {
  const user = useGET<User>(`/api/users/${id}`)
  return <div>{user.name}</div>
}

No queryKey. No invalidate. No staleTime. No provider. ~2KB gzipped (excluding peer deps).


When to reach for this

You want… Use this lib?
Single GET in a component, Suspense-integrated
A mutation (POST/PUT/PATCH/DELETE)
Optimistic UI with React 19's useOptimistic
Pagination, infinite scroll
Cache invalidation across a dependency graph
Background refetch on window focus
DevTools with request timeline

For anything in the ❌ column, reach for TanStack Query or SWR. This lib is intentionally the smaller of the two — keep both in the same app if you need.


Philosophy: no cache by default

TanStack Query and SWR are caches. This is not a cache. The mental model is closer to fetch() wrapped in Suspense.

  • A useGET call fires a request when it mounts.
  • Concurrent siblings on the same URL share one in-flight request (request coalescing — browsers don't dedupe concurrent requests, so forcing duplicates would just waste bandwidth).
  • Once the last consumer unmounts, the entry is evicted. A remount fires a fresh request.
  • Freshness (Cache-Control, ETag, 304) is the browser's HTTP cache's job. Service Workers can extend this further.

What you don't get:

  • No staleTime or gcTime — nothing to tune.
  • No queryKey — the URL + params IS the key.
  • No invalidate('users') — there's no cache to invalidate. Unmount + remount gets you fresh data.
  • No background refetch — that's a caching feature.

What you do get:

  • Predictability. If you see a useGET render and don't see a request in the network tab, something is broken.
  • No surprise stale renders after navigation.
  • Smaller mental model: Suspense fallback → data appears → unmount → gone.

Install

pnpm add @laststance/use-get axios

Peers: react@^19, axios@^1.7.


API

configureClient(config)

One-time setup. Call once at app startup. No provider needed.

import { configureClient } from '@laststance/use-get'

configureClient({
  baseURL: 'https://api.example.com',
  timeout: 10_000,
  headers: {
    Authorization: () => `Bearer ${getToken()}`, // functions re-evaluate per request
  },
})

useGET<T>(url, options?): T

Suspends while the request is pending. Throws to the nearest Error Boundary on failure.

const user = useGET<User>('/api/users/1')
const list = useGET<Post[]>('/api/posts', {
  params: { page: 1, _limit: 20 },
  retries: 3,      // default 3 — GET is idempotent, safe to retry
  timeout: 5_000,  // default 10_000
})

Always wrap in <Suspense> + <ErrorBoundary> upstream.

usePOST / usePUT / usePATCH / useDELETE

One hook per HTTP verb — method is visible at the call site instead of buried in config.

const { mutate, isPending, data, error, reset } = usePOST<CreateUser, User>({
  url: '/api/users',
  onSuccess: (user, req) => console.log('created', user),
  onError: (err, req) => console.error(err),
  retries: 0, // default 0 — non-idempotent, don't retry by default
})

await mutate({ name: 'Ada' })

Dynamic URL:

const { mutate } = useDELETE<{ id: number }, void>({
  url: (req) => `/api/users/${req.id}`,
})

Error helpers

import { getStatus, isClientError, isServerError, isRetryable } from '@laststance/use-get'

if (isClientError(err)) { /* 400–499 */ }
if (isServerError(err)) { /* 500–599 */ }
if (isRetryable(err))   { /* 408, 429, 500, 502, 503, 504, ECONNABORTED */ }

Optimistic updates (user-land)

Optimistic UI is not in this library. It's a React 19 primitive (useOptimistic) that you compose with the mutation hook:

import { useGET, usePOST } from '@laststance/use-get'
import { useOptimistic, useState, useTransition } from 'react'

function Todos() {
  const todos = useGET<Todo[]>('/api/todos')
  const [optimistic, addOptimistic] = useOptimistic(
    todos,
    (state, next: Todo) => [...state, next],
  )
  const { mutate } = usePOST<Partial<Todo>, Todo>({ url: '/api/todos' })
  const [, startTransition] = useTransition()

  function add(title: string) {
    startTransition(async () => {
      addOptimistic({ id: -Date.now(), title, completed: false })
      await mutate({ title, completed: false })
      // No cache to invalidate. Next unmount/remount fetches fresh.
    })
  }

  return <ul>{optimistic.map(t => <li key={t.id}>{t.title}</li>)}</ul>
}

Using it alongside TanStack Query

They coexist cleanly — they share axios (via configureClient) but have no other runtime overlap.

  • Use TanStack Query for: paginated lists, infinite scroll, cross-component cache coherence, anything needing invalidate.
  • Use this lib for: the one-off GET in a settings page or a dashboard tile where a useQuery({ queryKey: ['settings'], queryFn: ... }) feels like overkill.

Do / Don't

✅ Do

  • Wrap every useGET tree in both <Suspense> and <ErrorBoundary> (React 19 still requires a class component for Error Boundaries).
  • Call configureClient once, at module load — not inside a component.
  • Let concurrent siblings on the same URL share one request (that's coalescing working as intended).
  • Keep usePOST retries: 0 unless you're sure the endpoint is idempotent.

❌ Don't

  • Don't call useGET conditionally, inside loops, or after early returns. React's use() has the same rules as other hooks, plus more (see React docs on use).
  • Don't expect the library to dedupe requests after unmount. A remount of the same URL = a new request, by design.
  • Don't try to mutate the returned data. The Promise result is shared across concurrent consumers — treat it as immutable.
  • Don't use this in RSC (Server Components). For server-rendered data, await axios.get(url) directly — you don't need Suspense integration there.

Running the example

pnpm install
pnpm build
pnpm --filter vite-spa-example dev

Open http://localhost:5173. The example proxies /api/* to JSONPlaceholder so you can play with three demos (basic GET, URL-change, optimistic POST) without standing up a backend.


Testing

pnpm test         # Vitest Browser Mode (Playwright chromium)
pnpm typecheck

Real-browser tests — use() and Suspense behave differently in JSDOM, and the stakes of getting this wrong are too high to mock.


License

MIT © Laststance.io

About

React 19 use() + axios hooks with no long-lived cache. useGET / usePOST / usePUT / usePATCH / useDELETE.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors