React Data Fetching with useQm
React Data Fetching with useQm
In the modern React ecosystem, managing server state can often feel like a choice between two extremes: rolling your own useEffect logic (and all the boilerplate that comes with it) or reaching for a heavyweight library like TanStack Query.
useQm is something in between. useQuery and useMutation provide decoupled loading states, error handling, and type safety without the overhead of a full caching engine.
What is useQm exactly?
useQm is a lightweight, zero-dependency (except React) in-place alternative to libraries like React Query. It is designed not to be installed as a package, but to be copied directly into your project. This “copy-pasteable” philosophy gives a full control over the fetching logic while providing a structured way to handle queries and mutations.
Key Features
- Decoupled States: Easily access
data,loading, andproblemDetails(error details). - Problem Details Support: Support for the
application/problem+jsonstandard. - Auto-abort: Automatically cancels pending requests on unmount or new triggers.
- Type Safe: First-class TypeScript support out of the box.
What is not?
- useQm is not a caching engine. It does not cache data.
- useQm is not a full-featured data fetching library. It does not provide features like pagination or infinite scrolling.
- useQm is not a debouncing, auto-fetching or auto-retry library.
Getting Started: useQuery
The useQuery hook is for fetching data. It handles the manual useEffect dance and provides a clean interface for UI.
import { useQuery } from './hooks/useQm';
interface User {
id: string;
name: string;
}
function UserProfile() {
const { data: users, loading, problemDetails, query: refetch } = useQuery<User[]>('/api/users');
if (loading) return <div>Loading users...</div>;
if (problemDetails) return <div>Error: {problemDetails.title}</div>;
return (
<div>
<h1>Users</h1>
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
<button onClick={() => refetch()}>Refresh List</button>
</div>
);
}
By default, useQuery fetches data as soon as the component mounts, but this behavior (and many other fetch options) can be configured via the optional second argument.
Handling Actions: useMutation
When it’s time to modify data—whether it’s a POST, PUT, or DELETE - useMutation is for that. Unlike useQuery, mutations are triggered manually.
import { useMutation } from './hooks/useQm';
function AddUserButton() {
const { mutate, loading } = useMutation<User>('/api/users/create');
const handleAddUser = async () => {
const newUser = await mutate({
body: { name: 'Jane Doe' },
});
if (newUser) {
alert(`User ${newUser.name} added successfully!`);
}
};
return (
<button onClick={handleAddUser} disabled={loading}>
{loading ? 'Adding...' : 'Add User'}
</button>
);
}
Global Configuration with QmProvider
While useQm works standalone, main app can be wrapped in a QmProvider to handle global concerns like authentication headers or centralized error tracking.
import { QmProvider } from './hooks/useQm';
function App() {
return (
<QmProvider
getAuthHeader={async () => `Bearer ${await getToken()}`}
trackError={(err, details) => myLoggingService.log(err, details)}
>
<Main />
</QmProvider>
);
}
Why useQm?
The primary strength of useQm is its simplicity. It doesn’t try to solve every problem (like caching or debouncing).