I've been thinking about this issue recently and I came up with the following:
You could separate the data layer consumption on a use-case, and given its interface, on the presentation layer, you could have a hook that receives this use-case as a parameter (dependency inversion principle). By doing this, you might be able to not only segregate the react-query to the presentation layer but react itself. Finally, this hook can be used on any component by receiving the use-case as a prop.
In order to illustrate my thoughts, imagine the following scenario where we have an authentication flow:
First of all, on my data layer, I can have an Authentication use case, that exports all the methods needed, let's suppose it follows the interface below:
# data/use-cases/authentication.ts
export interface TAuthentication {
baseAuth: (params: { username: string, password: string }) => TUser
}
Where the baseAuth method implementation is the usage of the authentication service using some sort of client, HTTP for example.
On the presentation layer, we could now do the following:
# presentation/hooks/use-authentication.ts
import { useMutation } from 'react-query'
import { QUERY_KEYS } from 'constants/query-keys'
import type { TAuthentication } from 'domain/use-cases/authentication'
export type UseAuthenticationProps = {
authentication: TAuthentication
}
export function useAuthentication({ authentication }: UseAuthenticationProps) {
...
const baseAuthMudation = useMutation(QUERY_KEYS.BASE_AUTH_KEY, authentication.baseAuth)
...
return {
baseAuth: baseAuthMutation,
...
}
}
Finally, at some component that consumes this hook, we could do the following:
# presentation/pages/SignInPage
import { useAuthentication } from '../../hooks/use-authentication'
import type { TAuthentication } from 'domain/use-cases/authentication'
export type SignInPageProps = {
...
authentication: TAuthentication
}
export function SignInPage({ authentication }: SignInPageProps) {
...
const { baseAuth } = useAuthentication({ authentication })
...
const handleOnSubmit = async (params: { username: string, password: string }) => {
...
await baseAuth(params)
...
}
}
I still need to give this architecture a shot, but until now, it's the best that I could come up with ;).