https://github.com/laststance/use-get
React 19 use() + axios hooks with no long-lived cache. useGET / usePOST / usePUT / usePATCH / useDELETE.
https://github.com/laststance/use-get
axios hooks no-cache react react-19 suspense typescript use-hook vibe-coding
Last synced: 25 days ago
JSON representation
React 19 use() + axios hooks with no long-lived cache. useGET / usePOST / usePUT / usePATCH / useDELETE.
- Host: GitHub
- URL: https://github.com/laststance/use-get
- Owner: laststance
- License: mit
- Created: 2026-04-23T12:18:58.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-23T12:19:01.000Z (3 months ago)
- Last Synced: 2026-06-06T22:12:17.715Z (about 1 month ago)
- Topics: axios, hooks, no-cache, react, react-19, suspense, typescript, use-hook, vibe-coding
- Language: TypeScript
- Size: 70.3 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# @laststance/use-get
Tiny React 19 hooks for axios — built on `use()` + Suspense, with **no long-lived cache**.
```tsx
import { useGET } from '@laststance/use-get'
function UserCard({ id }: { id: number }) {
const user = useGET(`/api/users/${id}`)
return
{user.name}
}
```
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](https://tanstack.com/query) or [SWR](https://swr.vercel.app/). 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
```bash
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.
```ts
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(url, options?): T`
Suspends while the request is pending. Throws to the nearest Error Boundary on failure.
```ts
const user = useGET('/api/users/1')
const list = useGET('/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 `` + `` upstream.
### `usePOST` / `usePUT` / `usePATCH` / `useDELETE`
One hook per HTTP verb — method is visible at the call site instead of buried in config.
```ts
const { mutate, isPending, data, error, reset } = usePOST({
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:
```ts
const { mutate } = useDELETE<{ id: number }, void>({
url: (req) => `/api/users/${req.id}`,
})
```
### Error helpers
```ts
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:
```tsx
import { useGET, usePOST } from '@laststance/use-get'
import { useOptimistic, useState, useTransition } from 'react'
function Todos() {
const todos = useGET('/api/todos')
const [optimistic, addOptimistic] = useOptimistic(
todos,
(state, next: Todo) => [...state, next],
)
const { mutate } = usePOST, 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
- {optimistic.map(t =>
- {t.title} )}
}
```
---
## 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 `` and `` (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`](https://react.dev/reference/react/use#usage)).
- 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
```bash
pnpm install
pnpm build
pnpm --filter vite-spa-example dev
```
Open http://localhost:5173. The example proxies `/api/*` to [JSONPlaceholder](https://jsonplaceholder.typicode.com/) so you can play with three demos (basic GET, URL-change, optimistic POST) without standing up a backend.
---
## Testing
```bash
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