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 datafetchQuery(<new api object>)
- imperative method-fetched dataprefetchQuery(<new api object>)
- imperative method-fetched data for SSR cacheinvalidateQuery(<new api object>)
- invalidate the api's cache keymockApiResponse(<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?
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
}
}
// 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