How to use separation of concern with react-query (in a clean architecture context)
Asked Answered
J

4

6

I'm currently thinking about the perfect architecture for my professionals projects needs. I read a lot of article about (clean) architecture and I got to the point were I think that I want my UI managed with React totally separated from the application business logic that will be managed by "application manager". The issue is that I want the "application manager" to config and trigger mutations (I think get queries can be used in components without any issue). But since react-query require it to be in React component by using hooks, I don't think it is possible.

I am wrong ?

Does it exist a workaround ?

Maybe you have a library that manage that better ? I'm thinking about RTK Query maybe...

Jeannettajeannette answered 18/3, 2022 at 10:19 Comment(3)
If you create a hook to manage all the queries in your app independently from your app business logic, it would still be separate. I don't see much benefit in wanting it to be "outside of react". react-query exposes hooks, so you can create a hook that wraps it and take it from there to create your application manager.Anabaptist
@ben, I think you are right, I will test it. At the beginning, i wanted to be able to change the ui framework with ease if needed (that what clean architecture is about). That's why I did not want to depend to React Hooks. But realistically, there no reason to change especially since most of the project are built with React Native. Thank you for your help !Jeannettajeannette
If you want to have your logic make side effects, then the common issue is that those effects will run on a per component basis. Fx show a notification. You would have to put that into your fetcher instead. If you use codegen, orval etc, then that is not really an option.Possessive
V
3

I am a heavy user of RQ for quite some time and since architecture question can never have an objectively correct answer, I can demonstrate what I do personally.

First, I extract all queries and components into API modules by domain, given a simple app with posts, authors and comments, I would have files along these lines with those exports:

// apis/posts.js
export function useGetPosts() {}
export function useGetPost(postId) {}
export function usePutPost() {}
export function usePostPost() {}
export function useDeletePost() {}

// apis/comments.js
export function useGetComments(postId) {}
export function useGetComment(commentId) {}
export function usePutComment() {}
export function usePostComment() {}
export function useDeleteComment() {}

// apis/authors.js
export function useGetAuthors() {}
export function useGetAuthor(authorId) {}
export function usePutAuthor() {}
export function usePostAuthor() {}
export function useDeleteAuthor() {}

Each of those modules would internally handle everything necessary to work as a whole, like useDeleteAuthor would have a mutation and also modify the cache on success, or possibly implement optimistic updates.

Each will have a system of query keys so that the consumer (your components) don't have to know a thing about them.

function MyComponent() {
  const posts = useGetPosts()
}

function MyOtherComponent() {
  const deletePost = useDeletePost()
}

Try to make the APIs as complete as possible, but also don't forget that mutations can, for example, accept callbacks on call-site:

deletePost.mutate(payload, {
  onMutate: () => setState(false)
})

Let's assume you can use this to for example close a confirmation modal before deleting. Something like this doesn't belong to API module, so we just provide it as a local callback to the mutation.

As stated above, there is no correct answer. There is definitely an argument for doing it the other way round and using collocation more, putting queries next to the components where you are using them. But if you want separation, this would be a place to start in my opinion.

As Ben wrote in the comment to your question, RQ is just hooks, so I agree that trying to put it "outside of react" is non-sensical.

Variole answered 22/3, 2022 at 14:48 Comment(2)
Trying to remove dependencies from your logic is not something that I would call nonsensical.Possessive
Sorry, but your answer doesn't make sense. It looks like you want to use umph-query instead of react-query to remove the dependency on react. useQuery is a hook, if you want to use hooks, you do so inside a component. If you don't want to have it inside a component, don't use a hook based library.Variole
L
2

You're right, the short answer is react-query is not compatible with clean architecture, and by experience it leads to tight coupling between logic and components

Longinus answered 4/11, 2022 at 17:5 Comment(0)
P
0

One way that I'm experimenting with is using the queries in components as is, without implementing side effects. Unless it is side effects specifically for that components.

Then inside my logic layer, I would use the QueryObserver and subscribe to changes to whatever key/keys I need.

const observer = new QueryObserver(myQueryClient, {
   queryKey: ['key']
})
observer.subscribe(result => console.log(result))

In this example I have my queryClient defined in its own file.

This way I can have my logic seperated from the view layer, but still use the awesome way react-query works.

Note that this way, the logic will only run when a component is mounted that the query function is resolved.

Also the subscibe function can only be called after the inital useQuery is mounted. Else you will get a "Missing queryFn" error. Which is not ideal. Or even close.

Possessive answered 26/1, 2023 at 14:42 Comment(0)
S
0

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 ;).

Sesquipedalian answered 29/6, 2023 at 3:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.