DEV Community

Cover image for Improving API Documentation using React Query and TypeScript
Arnav Gosain
Arnav Gosain

Posted on • Updated on • Originally published at arnavgosain.com

Improving API Documentation using React Query and TypeScript

As your codebase grows, there is only one way to increase developer productivity: Documentation. One of many reasons I prefer TypeScript to JavaScript is that overtime, as your codebase grows, developer productivity increases because TypeScript (and typed languages in general) offer something that dynamically typed languages cannot, automatic documentation in your IDE.

This article assumes you're familiar with React Query. If you're not, I highly recommend you read the official docs and this intro guide by Sai Kranthi.

Why React Query

Imagine an simple app that does two things based on the PokeAPI:

  1. Renders a list of Pokemons that link to their own dedicated page
  2. Have dedicated pages for all pokemons

To fetch the list of pokemons, with Redux (before RTK Query) you would have to:

  1. Create a global store
  2. Create a reducer with an action to update the list in the store
  3. Write a thunk action to fetch the data.
  4. Write a useEffect hook inside to dispatch the thunk action.
  5. Render the list.

And then you'd have to write invalidation logic, loading status logic and much more.

But with React Query, fetching your list of pokemons is as easy as wrapping your App in a QueryClientProvider and then making use of the useQuery and useMutation hooks.

Example of basic React Query usage:

This approach works for simple apps like a Pokemon List, but it quickly becomes unmanageable as you add more endpoints to your API. In which case, you would have to create many such custom hooks.

This is the problem I ran into as I hopped on my first project after joining TartanHQ. While it's a fairly simple CRUD app, it makes use of many endpoints and making custom hooks for each endpoint simply isn't an option.

One Hook For All Queries

To counteract this problem, we created a layer of abstraction over React Query's useQuery hook, a hook that makes use of TypeScript to improve discoverability of endpoints across the entire application.

import * as React from "react";
import {
  useQuery as useReactQuery,
  UseQueryOptions,
  UseQueryResult,
} from "react-query";
import { queryFetchers, QueryKeys } from "~/lib/api/queries";

type Await<T>  = T extends Promise<infer U> ? U : T;

export function useQuery<
  Key extends QueryKeys,
  Params = Parameters<typeof queryFetchers[Key]>,
  Data = Await<ReturnType<typeof queryFetchers[Key]>>
>(key: Key, options?: UseQueryOptions<Data>): UseQueryResult<Data>;

export function useQuery<
  Key extends QueryKeys,
  Params = Parameters<typeof queryFetchers[Key]>,
  Data = Await<ReturnType<typeof queryFetchers[Key]>>
>(
  key: Key,
  params: Params,
  options?: UseQueryOptions<Data>
): UseQueryResult<Data>;

export function useQuery<
  Key extends QueryKeys,
  Params = Parameters<typeof queryFetchers[Key]>,
  Data = Await<ReturnType<typeof queryFetchers[Key]>>
>(
  key: Key,
  arg2?: Params | UseQueryOptions<Data>,
  arg3?: UseQueryOptions<Data, unknown, Data>
) {
  const params = Array.isArray(arg2) ? arg2 : [];
  const options = !!arg3 && Array.isArray(arg2) ? arg3 : arg2;

  return useReactQuery(
    key,
    () => queryFetchers[key].apply(null, params),
    options
  );
}
Enter fullscreen mode Exit fullscreen mode
/**
 * Legend:
 *
 * QKEY = Query Key
 * QData = Query Data
 */

const GET_ALL_POKEMONS_QKEY = "pokemons/all" as const;
type GetAllPokemonsQData = {
  count: number;
  next: string;
  previous: string;
  results: { name: string; url: string }[];
};
const getAllPokemons = (): Promise<GetAllPokemonsQData> => {
  return fetch("https://pokeapi.co/api/v2/pokemon?limit=151").then(
    (response) => response.json() as GetAllPokemonsQData
  );
};

const POKEMON_BY_ID_QKEY = "pokemons/byId" as const;
type GetPokemonByIdQData = Record<string, unknown>;
const getPokemonById = (id: string) => {
  return fetch(`https://pokeapi.co/api/v2/pokemon/${id}/`).then(
    (res) => res.json() as GetPokemonByIdQData
  );
};

export type QueryKeys = typeof GET_ALL_POKEMONS_KEY | typeof POKEMON_BY_ID_QKEY;
export const queryFetchers = {
  [GET_ALL_POKEMONS_QKEY]: getAllPokemons,
  [POKEMON_BY_ID_QKEY]: getPokemonById,
} as const;
Enter fullscreen mode Exit fullscreen mode

Example:

Now that you're all done, you can take full advantage of VSCode autocomplete.

Custom useQuery in Action


If you have an alternative idea or found this useful: I'd love to connect with you on Twitter!

Discussion (2)

Collapse
phryneas profile image
Lenz Weber • Edited on

To be fair, the above does describe a valid way of doing this with Redux - but the official Redux Toolkit also contains "RTK Query", which does essentially the same as React Query. In RTK Query, a similar functionality would look like

// Need to use the React-specific entry point to import createApi
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { Pokemon } from './types'

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
  endpoints: (builder) => ({
    getPokemonById: builder.query<Pokemon, string>({
      query: (id) => `pokemon/${id}`,
    }),
    getAllPokemon:  builder.query<Record<string,Pokemon>, void>({
      query: (id) => `pokemons/all`,
    }),
  }),
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByIdQuery, useGetAllPokemonQuery } = pokemonApi
Enter fullscreen mode Exit fullscreen mode

And this either integrates in your Redux store or you don't set a Redux store up, but just wrap your app into a <ApiProvider api={pokemonApi}> which will automatically set up a Redux store for you.

If you're interested in more, check out redux-toolkit.js.org/tutorials/rtk...

If you want to play around with it, here is a CodeSandbox: codesandbox.io/s/github/reduxjs/re...

Collapse
arn4v profile image
Arnav Gosain Author

I haven't had a chance to try RTK Query, will try it out!

Not mentioning RTK Query is an oversight on my end, I have edited the post to mention it now.