More Examples
React (no TanStack)
Use typed Axios with React useEffect and useState — OpenAPI contracts without React Query.
You don't need TanStack Query to benefit from typed Axios. You need a component that respects { data, error } and doesn't pretend loading is optional.
Want the full query-keys / loader stack later? → TanStack Query Example.
List + detail with useEffect
import { useEffect, useState } from "react";
import { api } from "@/services/api/api.client";
import type { UserDto } from "@/services/api/_generated/api.contracts";
import type { ApiError } from "@/services/api/api.error";
export function UserList() {
const [users, setUsers] = useState<UserDto[] | null>(null);
const [error, setError] = useState<ApiError | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
(async () => {
const { data, error } = await api.op.listUsers();
if (cancelled) return;
setLoading(false);
if (error) {
setError(error);
return;
}
setUsers(data);
})();
return () => {
cancelled = true;
};
}, []);
if (loading) return <p>Loading…</p>;
if (error) return <p role="alert">{error.message}</p>;
return (
<ul>
{users?.map((user) => (
<li key={user.id}>{user.email}</li>
))}
</ul>
);
}Tiny hook (still not React Query)
import { useEffect, useState } from "react";
import { api } from "@/services/api/api.client";
import type { UserDto } from "@/services/api/_generated/api.contracts";
import type { ApiError } from "@/services/api/api.error";
export function useUser(id: string) {
const [user, setUser] = useState<UserDto | null>(null);
const [error, setError] = useState<ApiError | null>(null);
const [loading, setLoading] = useState(Boolean(id));
useEffect(() => {
if (!id) return;
let cancelled = false;
setLoading(true);
api.op.getUser({ id }).then(({ data, error }) => {
if (cancelled) return;
setLoading(false);
if (error) {
setError(error);
setUser(null);
return;
}
setError(null);
setUser(data);
});
return () => {
cancelled = true;
};
}, [id]);
return { user, error, loading };
}Mutations without a library
async function onCreate(body: CreateUserBody) {
const { data, error } = await api.op.createUser(body);
if (error) {
setFormError(error.message);
return;
}
// refresh list or navigate
setUsers((prev) => (prev ? [...prev, data] : [data]));
}When to graduate to TanStack
- Same resource fetched in three places
- You need cache, stale-while-revalidate, or route loaders
- Invalidation after mutations is getting inventive
Until then, contracts + api.op + honest loading state is enough.