OrvalOrval

React Query

Generate type-safe React Query hooks from OpenAPI

Generate fully typed TanStack Query hooks from your OpenAPI specification.

Configuration

Set the client option to react-query:

orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      mode: 'tags-split',
      target: 'src/api/petstore.ts',
      schemas: 'src/api/model',
      client: 'react-query',
      mock: true,
    },
    input: {
      target: './petstore.yaml',
    },
  },
});

Generated Output

Orval generates one custom hook per path in your OpenAPI specification. For example, this specification generates:

export const showPetById = (
  petId: string,
  options?: AxiosRequestConfig,
): Promise<AxiosResponse<Pet>> => {
  return axios.get(`/pets/${petId}`, options);
};

export const getShowPetByIdQueryKey = (petId: string) => [`/pets/${petId}`];

export const useShowPetById = <
  TData = AsyncReturnType<typeof showPetById>,
  TError = Error,
>(
  petId: string,
  options?: {
    query?: UseQueryOptions<AsyncReturnType<typeof showPetById>, TError, TData>;
    axios?: AxiosRequestConfig;
  },
) => {
  const { query: queryOptions, axios: axiosOptions } = options ?? {};

  const queryKey = queryOptions?.queryKey ?? getShowPetByIdQueryKey(petId);
  const queryFn = () => showPetById(petId, axiosOptions);

  const query = useQuery<AsyncReturnType<typeof queryFn>, TError, TData>(
    queryKey,
    queryFn,
    {
      enabled: petId !== null && petId !== undefined,
      ...queryOptions,
    },
  );

  return {
    queryKey,
    ...query,
  };
};

Infinite Queries

Generate useInfiniteQuery hooks alongside regular queries:

orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      client: 'react-query',
      override: {
        query: {
          useQuery: true,
          useInfinite: true,
          useInfiniteQueryParam: 'nextId',
          options: {
            staleTime: 10000,
          },
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});

Per-Operation Overrides

Override query options for specific operations:

orval.config.ts
import { defineConfig } from 'orval';

export default defineConfig({
  petstore: {
    output: {
      client: 'react-query',
      override: {
        operations: {
          listPets: {
            query: {
              useInfinite: true,
              useInfiniteQueryParam: 'cursor',
            },
          },
        },
      },
    },
    input: {
      target: './petstore.yaml',
    },
  },
});

Set Query Data

When useSetQueryData: true is set, Orval generates type-safe helper functions to update cached query data:

export const useSetListPetsQueryData = () => {
  const queryClient = useQueryClient();
  return (
    params: ListPetsParams | undefined,
    updater:
      | Awaited<ReturnType<typeof listPets>>
      | undefined
      | ((
          old: Awaited<ReturnType<typeof listPets>> | undefined,
        ) => Awaited<ReturnType<typeof listPets>> | undefined),
    $exactMatch: boolean = true,
  ) => {
    queryClient.setQueriesData<Awaited<ReturnType<typeof listPets>>>(
      { exact: $exactMatch, queryKey: getListPetsQueryKey(params) },
      updater,
    );
  };
};

The helper uses setQueriesData so query keys can be matched by prefix. Pass $exactMatch: false to match and update every cached entry sharing the given path prefix. For endpoints with query params or a body, those args are widened to accept undefined — pass undefined (together with $exactMatch: false) to update every cached entry sharing the same path; the updater is invoked once per matched entry.

Prior to 8.11.0, the helper called setQueryData, which writes (and creates) a single exact-key entry. Since 8.11.0 the helper calls setQueriesData, which only updates entries that already exist (it never creates one), and as of this change it matches the key exactly by default. Pass $exactMatch: false to match by prefix instead. Review existing call sites when upgrading to confirm they still behave as expected.

Pass undefined for query params or body, and set $exactMatch to false, to update every cached entry that shares the same path — useful when a mutation should patch a record across all filter/pagination variants:

const updatePetList = useSetListPetsQueryData();
updatePetList(
  undefined,
  // invoked once per matched cache entry — return the new shape for that entry
  (old) => old?.map((pet) => (pet.id === updated.id ? updated : pet)),
  false,
);

Get Query Data

When useGetQueryData: true is set, Orval generates type-safe helper functions to read cached query data:

export const useGetListPetsQueryData = () => {
  const queryClient = useQueryClient();
  return (params: ListPetsParams) =>
    queryClient.getQueryData<Awaited<ReturnType<typeof listPets>>>(
      getListPetsQueryKey(params),
    );
};

Skip Token

When useSkipToken: true is set, a query whose params are not resolved yet is held with skipToken rather than the generated enabled guard:

export const getShowPetByIdQueryOptions = <...>(
  petId: string,
  options?: {...},
) => {
  const queryKey = queryOptions?.queryKey ?? getShowPetByIdQueryKey(petId);
  const queryFn: QueryFunction<...> = ({ signal }) => showPetById(petId, signal);

  return {
    queryKey,
    queryFn: petId === null || petId === undefined ? skipToken : queryFn,
    ...queryOptions,
  } as ...;
};

This matters in two cases the enabled guard does not cover:

  • enabled does not gate refetch(), so a retry button sends the request with the unresolved param in the URL. With skipToken no request is sent — the query rejects with Missing queryFn instead, which is the tradeoff to know about before enabling the option.
  • enabled is a single option and ...queryOptions is spread last, so useShowPetById(petId, { query: { enabled: isTabVisible } }) replaces the generated param check. With skipToken the two gates are independent.

Pair it with allParamsOptional so callers can pass an unresolved param without a cast.

Suspense queries are unaffected: TanStack excludes SkipToken from their queryFn, which is also why they get no enabled guard.

Full Example

See the complete React Query example on GitHub.

On this page