DEV Community

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

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

4

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!

Auth0

Auth0 now, thank yourself later. 😌

Our Free plan just got better! Custom Domain, Okta connections, and more—all included. Start building with up to 25,000 MAUs today.

Try free today

Top comments (2)

Collapse
 
phryneas profile image
Lenz Weber • Edited

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

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.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay