Skip to main content

Best Practices

This guide covers best practices for using @navios/react-query effectively in your React applications.

Organizing Queries and Mutations

Separate Endpoints from Queries

// ✅ Good - endpoints in shared package
// shared/endpoints/users.ts
export const getUserEndpoint = API.declareEndpoint({
method: 'GET',
url: '/users/$userId',
responseSchema: userSchema,
})

// client/queries/users.ts
const getUser = client.queryFromEndpoint(getUserEndpoint, {
processResponse: (data) => data,
})

Group by Resource

// ✅ Good - organized by resource
// client/queries/users.ts
export const getUser = client.query(...)
export const getUsers = client.query(...)

// client/mutations/users.ts
export const createUser = client.mutation(...)
export const updateUser = client.mutation(...)
export const deleteUser = client.mutation(...)

Query Patterns

Use Suspense When Possible

// ✅ Good - cleaner with Suspense
function UserProfile({ userId }: { userId: string }) {
const user = getUser.useSuspense({ urlParams: { userId } })
return <div>{user.name}</div>
}

// ❌ More verbose without Suspense
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = getUser.use({ urlParams: { userId } })
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{data.name}</div>
}

Use processResponse for Transformations

// ✅ Good - transform at query level
const getUser = client.query({
method: 'GET',
url: '/users/$userId',
responseSchema: userSchema,
processResponse: (data) => ({
...data,
displayName: `${data.firstName} ${data.lastName}`,
}),
})

// ❌ Bad - transform in component
const getUser = client.query({ ... })
function Component() {
const { data } = getUser.use({ ... })
const displayName = `${data.firstName} ${data.lastName}`
}

Mutation Patterns

// ✅ Good - invalidate after mutation
const createUser = client.mutation({
// ...
useContext: () => ({ queryClient: useQueryClient() }),
onSuccess: (data, variables, context) => {
context.queryClient.invalidateQueries({ queryKey: ['users'] })
},
})

Use Optimistic Updates

// ✅ Good - optimistic updates for better UX
const updateUser = client.mutation({
// ...
onMutate: async (variables, context) => {
await context.queryClient.cancelQueries({ queryKey: ['users'] })
const previous = context.queryClient.getQueryData(['users', variables.urlParams.userId])
context.queryClient.setQueryData(['users', variables.urlParams.userId], {
...previous,
...variables.data,
})
return { previous }
},
onError: (error, variables, context) => {
if (context.onMutateResult?.previous) {
context.queryClient.setQueryData(
['users', variables.urlParams.userId],
context.onMutateResult.previous
)
}
},
})

Use Scoped Mutations for Lists

// ✅ Good - track mutations per item
const updateUser = client.mutation({
// ...
useKey: true,
})

function UserList() {
return users.map((user) => (
<UserCard key={user.id} user={user} />
))
}

function UserCard({ user }: { user: User }) {
const { mutate } = updateUser({ urlParams: { userId: user.id } })
const isUpdating = updateUser.useIsMutating({ userId: user.id })
// ...
}

Query Key Management

Use Helper Methods

// ✅ Good - use helper methods
await getUser.invalidate(queryClient, { urlParams: { userId: '123' } })
await getUser.invalidateAll(queryClient, { urlParams: { userId: '123' } })

// ❌ Bad - manual key construction
await queryClient.invalidateQueries({
queryKey: ['users', '123'], // May not match actual key structure
})
// ✅ Good - invalidate all related queries
const updateUser = client.mutation({
// ...
onSuccess: (data, variables, context) => {
getUser.invalidateAll(context.queryClient, {
urlParams: { userId: data.id },
})
getUserPosts.invalidateAll(context.queryClient, {
urlParams: { userId: data.id },
})
},
})

Error Handling

Use Error Boundaries with Suspense

// ✅ Good - ErrorBoundary handles errors
<ErrorBoundary fallback={<ErrorDisplay />}>
<Suspense fallback={<LoadingSpinner />}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>

Provide User-Friendly Messages

// ✅ Good - user-friendly error messages
const createUser = client.mutation({
// ...
onError: (error, variables, context) => {
if (error instanceof NaviosError) {
context.toast.error('Failed to create user. Please try again.')
} else {
context.toast.error('An unexpected error occurred.')
}
},
})

Performance

Use Stale Time

// ✅ Good - prevent unnecessary refetches
const { data } = getUser.use({
urlParams: { userId },
staleTime: 5 * 60 * 1000, // 5 minutes
})

Prefetch Data

// ✅ Good - prefetch on hover
function UserList() {
const queryClient = useQueryClient()

const handleMouseEnter = (userId: string) => {
queryClient.prefetchQuery({
queryKey: getUser.queryKey.dataTag({ urlParams: { userId } }),
queryFn: () => getUser({ urlParams: { userId } }),
})
}

return (
<div>
{users.map((user) => (
<div
key={user.id}
onMouseEnter={() => handleMouseEnter(user.id)}
>
{user.name}
</div>
))}
</div>
)
}

Type Safety

Let TypeScript Infer Types

// ✅ Good - let TypeScript infer
const user = getUser.useSuspense({ urlParams: { userId: '123' } })
// user is automatically typed

// ❌ Bad - unnecessary type annotations
const user: User = getUser.useSuspense({ urlParams: { userId: '123' } })

Extract Types When Needed

// ✅ Good - extract types for reuse
type GetUserParams = Parameters<typeof getUser.use>[0]
type GetUserResponse = Awaited<ReturnType<typeof getUser.use>>['data']

Common Patterns

Conditional Queries

// ✅ Good - use enabled option
const { data } = getUser.use({
urlParams: { userId: userId! },
enabled: !!userId,
})

Dependent Queries

// ✅ Good - chain queries
function UserPosts({ userId }: { userId: string }) {
const { data: user } = getUser.use({ urlParams: { userId } })
const { data: posts } = getPosts.use({
urlParams: { userId },
enabled: !!user,
})
// ...
}

Next Steps