CodeLeap Docs

CommonWebMobileCLIConceptsUpdates

Why and how we use React query

To start off, read about the motivation for React Query, and why it does not replace redux entirely.

Structural guidelines

Suppose we have a very simple profile API.

import { api } from '@/app'
import { AppStatus } from '@/redux'
import { useMutation, useQuery, useQueryClient } from 'react-query'
export type Profile = {
id: number
first_name: string
last_name: string
email: string
avatar: string
}
const BASE_URL = 'profile/'
export async function create(data: Omit<Profile, 'id'>) {
const response = await api.post<Profile>(BASE_URL, data)
return response.data
}
export async function update(profile: Partial<Profile>) {
const response = await api.patch<Profile>(`${BASE_URL}/${profile.id}/`, profile)
return response.data
}
export async function retrieve() {
const response = await api.get<Profile>(`${BASE_URL}/i/`)
return response.data
}
export const QUERY_KEYS = {
retrieve: 'retrieveProfile',
update: 'updateProfile',
create: 'createProfile',
}
export function useProfile() {
const queryClient = useQueryClient()
const getProfile = useQuery(QUERY_KEYS.retrieve, retrieve)
// Whenever this function is called, our query will be refetched, either in the foreground or in the background
const invalidateQuery = () => queryClient.invalidateQueries(QUERY_KEYS.retrieve)
const updateProfile = useMutation(QUERY_KEYS.update, update, {
// This means 'when the request succeeds, invalidate the query'
onSuccess: invalidateQuery,
// The onMutate runs whenever we start the mutation, eg: call mutation.mutateAsync
onMutate: () => AppStatus.set('loading'),
// This is the last thing that runs after calling the mutation
onSettled(_, err) {
if (!err) {
AppStatus.set('done')
} else {
AppStatus.set('idle')
}
},
})
const createProfile = useMutation(QUERY_KEYS.create, create, {
onSuccess: invalidateQuery,
onMutate: () => AppStatus.set('loading'),
onSettled(_, err) {
if (!err) {
AppStatus.set('done')
} else {
AppStatus.set('idle')
}
},
})
return {
profile: getProfile.data,
// You should define additional properties for the return of the hook
// according to necessity, and reuse them on your components
isAuthenticated: getProfile.isSuccess,
createProfile,
updateProfile,
getProfile
}
}

1. One function per API route

Notice that each of the functions simply wraps an API call. This ensures we don't get messy with urls, and allows for easy alteration of both types and data formatting should it be necessary. It also has the benefit of facilitating debugging, since for a given route everything will go through the function anyway.

This will integrate nicely with react query since it accepts functions as arguments.

2. Try to keep types accurate

You don't need to be a typescript wizard to at least define the properties of what you expect on the function parameters, and what the API returns. Since this is a pretty central place, the rest of the project should automatically benefit from whatever types are defined here.

If you need some help with Typescript, read the TS 101.

3. Let errors be errors

This depends a lot on how the API responds to failure:

If it returns readable, user friendly errors, just throw an error with the error message or a fallback if there's no message (see create above).

Otherwise, a generic error message should suffice (see update above)

These should get intercepted by react query and be really easy to show on the UI.

4. Wrap react-query hooks with a hook for the resource itself

Instead of calling useQuery and useMutation on the components themselves, the logic is wrapped inside the useProfile hook. This will help in DRYing component code as well as preventing mistakes such as incorrectly referencing query keys or mismanaging load states.

This is great for handling unique resources such as the user's profile, but may quickly get messy and/or repetitive for list like data, which is quite common in most apps.

Pagination

Suppose we have a generic list of items. Our API functions don't change much, but the hook does.

Defining our hook with usePagination

Using the pagination hook, our API should look like this.

import { usePagination } from '@codeleap/common'
const LIMIT = 20
export function useItems(itemId?: string) {
const pagination = usePagination('items',{
// These are the functions that actually make the request
onList: api.listItems,
onCreate: api.createItem,
onRemove: api.removeItem,
onUpdate: api.updateItem,
onRetrieve: retrieveItem,
// With this, we can run arbitrary code before and after mutations
beforeMutate: () => AppStatus.set('loading'),
afterMutate: (operation, result) => AppStatus.set(result.status === 'error' ? 'idle' : 'done'),
// This is used internally to compare and index items
keyExtractor: item => item.id,
// This triggers a fetch for a single item and inserts into the list
where: [itemId],
sort(a, b) {
return new Date(b.created_datetime).getTime() - new Date(a.created_datetime).getTime()
},
// This is the limit our `api.listItems` function will receive. It's also used internally to determine initalState
limit: LIMIT,
// This will be used for automatically getting the text for flatlists
itemName: 'CoolItem',
// The hook can't possibly cover every single use case in existance, so you may override it on a query per query basis.
overrides: {
list: {
refetchOnMount: false
},
},
})
return pagination
}

Using the hook

And we may use it in components like so

function MyComponent() {
const itemsApi = useItems()
function onRemove(item){
itemsApi.queries.remove.mutateAsync(item.id)
}
function onCreate(values){
itemsApi.queries.create.mutateAsync(item)
}
return <List
// This includes the logic for refresh,
// showing pagination indicators, fetching more pages and other stuff
{...itemsApi.flatListProps}
renderItem={({ item }) => {
return <ItemCard
key={item.id}
onRemove={() => onRemove(item)}
item={item}
/>
}}
ListHeaderComponent={() => <CreateItemForm onSubmit={onCreate} />}
/>
}

Table of contents

Structural guidelines
1. One function per API route