chowbea-axios
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

components/UserList.tsx
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)

hooks/useUser.ts
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.

Next Steps

On this page