Skip to content

Instantly share code, notes, and snippets.

@timkindberg
Created July 21, 2022 00:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timkindberg/115240879d0943c909ec538fd2127c64 to your computer and use it in GitHub Desktop.
Save timkindberg/115240879d0943c909ec538fd2127c64 to your computer and use it in GitHub Desktop.
React Query Portable API Object Pattern

A New Portable API Object

What if we could construct an object that described everything about the fetch and query all together in a single object. And that object could be ultra-portable, able to be passed to any sort of functions such as:

  • useQuery(<new api object>) - declarative hook-fetched data
  • fetchQuery(<new api object>) - imperative method-fetched data
  • prefetchQuery(<new api object>) - imperative method-fetched data for SSR cache
  • invalidateQuery(<new api object>) - invalidate the api's cache key
  • mockApiResponse(<new api object>) - mock the api for a test

The inspiration was after seeing that useQuery can be passed a single object as an optional signature.

useQuery({
  queryKey: ...
  queryFn: ...
  config: ...
})

The object there has a lot of good details, what if it just had a little bit more like url and query parameters?

Constructing the Object using a Factory Pattern

Here’s what it might look like. The keys pattern is taken from Effective React Query Keys and it allows easier consistent targeting of keys. The rest is a new concept.

interface FooParams { a: number; b: number }
interface FooResponse { bar: boolean; baz: boolean }

export const fooKeys = {
  all: ['foo'] as const,
  lists: () => [...fooKeys.all, 'list'] as const,
  list: (query?: FooParams) => [...fooKeys.lists(), { query }] as const,
}

const fooApi = {
  list: (query?: FooParams) =>
    apiObj<AccountMeResponse>({
      queryKey: fooKeys.me(filters),
      url: `/api/v2/foo/`,
      query,
      config: { staleTime: 1000 * 60 * 5 }
    })
}

The apiObj utility would give TS completion and also construct the queryFn. It would also handle the boilerplate of stringifying the query object to a string.

export function apiObj(obj) {
  const method = obj.method ?? 'get'
  const getFullUrl = () => {
    const queryParams = qs.stringify(obj.query, { addQueryPrefix: true })
    return obj.url + queryParams
  }
  return {
    method,
    queryFn: () => {
      fetch[method](getFullUrl())
    },
    get fullUrl() {
      return getFullUrl()
    } 
    ...obj
  }
}

Example Usages of the Factory Function

// normal query usage
const { data } = useQuery(fooApi.list({ a: 1 }))

// fetch a query and 
// store it at the key ['foo', 'list', { a: 1 }]
useEffect(() => {
  queryCache.fetchQuery(fooApi.list({ a: 1 })).then(foo => setFoo(foo))
}, [])

// prefetch a query on the server and 
// store it at the key ['foo', 'list', { a: 1 }]
queryCache.prefetchQuery(fooApi.list({ a: 1 }))

// invalidates the key ['foo', 'list', { a: 1}]
invalidateQuery(fooApi.list({ a: 1 })) // *

// mock the response to this url with this specific query params
mockApiResponse(fooApi.list({ a: 1 }), { bar: true, baz: true }) // **

* We’d have to make a custom invalidateQuery function because the real one doesn’t accept the object ** We’d have to make our own mockApiResponse function

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment