Portable API Object Pattern Idea #3888
Replies: 2 comments 1 reply
-
This ... already works 😅 See: https://twitter.com/ralex1993/status/1570036707134676994 useQuery and all the imperative functions also accept an object with In fact, we plan to make this the only valid syntax in v5: (see "remove overloads") |
Beta Was this translation helpful? Give feedback.
-
I've worked on this some more and I'm pretty happy with how it's coming along. It takes the idea of a config factory a step farther, because I really want the url and query params of the api to be exposed and introspectable... currently that's not possible without defining them outside somewhere. But I want ONE util where you can declare an API and it's compatible with not only react-query functions but other functions that might be interested in see things like the url or query params. Here is an example of how it might look like to work with: // ~/apis/foo.ts
const fooBaseKey = ['foo']
type Foo = any
type FooListParams = { offset?: number; limit?: number }
export const fooListApi = createApi((args: FooListParams = {}) => ({
queryKey: [...fooBaseKey, 'list'],
path: `/api/v2/foo/`,
query: { offset: args.offset ?? 0, limit: args.limit ?? 100 },
select: response => response as Foo[]
}))
type FooDetailParams = { id?: number }
export const fooDetailApi = createApi((args: FooDetailParams = {}) => ({
queryKey: [...fooBaseKey, 'detail', args.id],
path: `/api/v2/foo/${args.id}`,
select: response => response as Foo,
config: {
enabled: Number.isInteger(args.id)
}
})) The fooListApi and fooDetailApi are just config factories. So you can see what they would return here: console.log(fooListApi({ offset: 100 }))
// {
// queryKey: ['foo', 'list'],
// method: 'get',
// url: '/api/v2/foo/?offset=100&limit=100',
// path: '/api/v2/foo/',
// query: {
// offset: 100,
// limit: 100
// },
// config: {},
// queryFn: () => fetch('/api/v2/foo/?offset=100&limit=100', { method: 'get' })
// }
console.log(fooDetailApi({ id: 5 }))
// {
// queryKey: ['foo', 'detail', 5],
// method: 'get',
// url: '/api/v2/foo/5',
// path: '/api/v2/foo/5',
// config: {
// enabled: true
// },
// queryFn: () => fetch('/api/v2/foo/5', { method: 'get' })
// } Now in your component you'd use it just like above, because the returned object is fully compatible with react-query: const FooList = () => {
const { data } = useQuery(fooListApi())
...
}
const FooDetail = (id: number) => {
const { data } = useQuery(fooDetailApi({ id }))
...
} Additionally you get properly typed const FooDetail = (id: number) => {
const { data } = useQuery(fooDetailApi({
id,
queryOptions: {
onSuccess(data) {
console.log(data)
}
}
}))
...
} The code to make this all happen is here: import qs from 'qs'
import { QueryConfig, QueryKey } from 'react-query'
import { PrefetchQueryObjectConfig } from 'react-query/types/core/queryCache'
import fetch from '~/common/fetch'
type RestMethods = 'get' | 'post' | 'put' | 'patch' | 'delete'
interface ApiConfig<TResult, TQueryObj> {
queryKey: QueryKey
path: string
select: (response: unknown) => TResult
method?: RestMethods
query?: TQueryObj
config?: QueryConfig<TResult>
prefetch?: PrefetchQueryObjectConfig<TResult, unknown>['options']
// Maybe give devs lower level access to control the fetching directly
// fetchFn: (args: { method: RestMethods; url: string; path: string; query: string; queryObj: TQueryObj }) => TResult
}
interface ApiReturn<TResult, TQueryObj> {
queryKey: QueryKey
method: RestMethods
queryFn: () => Promise<TResult>
readonly url: string
path: string
query?: TQueryObj
config?: QueryConfig<TResult>
prefetch?: PrefetchQueryObjectConfig<TResult, unknown>['options']
}
export function createApi<TResult, TQueryObj, TArgs extends any>(fn: (args?: TArgs) => ApiConfig<TResult, TQueryObj>) {
return (args?: TArgs & { queryConfig?: QueryConfig<TResult> }): ApiReturn<TResult, TQueryObj> => {
const { method = 'get', ...obj } = fn(args)
const queryParams = qs.stringify(obj.query, { addQueryPrefix: true })
const url = obj.path + queryParams
const defaultQueryFn = (): Promise<TResult> => {
return fetch[method](url)
}
return {
queryKey: obj.queryKey,
method,
url,
queryFn: defaultQueryFn,
path: obj.path,
query: obj.query,
config: Object.assign(obj.config ?? {}, args?.queryConfig)
}
}
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I'd love to see what the community thinks about this idea for a new pattern! @TkDodo especially you since you already put so much thought into best practices on using React Query.
Portable API Object Pattern
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:
Currently supported
useQuery(<new api object>)
- declarative hook-fetched datafetchQuery(<new api object>)
- imperative method-fetched dataprefetchQuery(<new api object>)
- imperative method-fetched data for SSR cacheNewly supported capabilities with this expanded pattern
invalidateQuery(<new api object>)
- invalidate the api's cache keymockApiResponse(<new api object>)
- mock the api for a testThe inspiration was after seeing that useQuery can be passed a single object as an optional signature. So they've already provided us with some partial support for what we need.
The object there has a lot of good details, what if it just had a little bit more information like url and query parameters?
Example Usages
Normal query, already supported
Manually fetch a query, already supported (I think)
Prefetch a query, already supported (I think)
Invalidates the key ['foo', 'list', { a: 1}], invalidateQuery doesn't currently support an object
Mock a response for this url with this specific query params. Doesn't exist yet. And is outside the scope of this library.
The mockApiResponse is something we currently do with fetchMock, but I'd love to abstract that away and just pass it the same exact object that we are passing to all the react-query methods.
Constructing the Object using a Factory Pattern
Here’s what it might look like to create the
fooApi.list
api object factory. The keys pattern is taken from Effective React Query Keys and it allows easier consistent targeting of keys. The rest is a new concept.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.
Beta Was this translation helpful? Give feedback.
All reactions