UseCases in React - Introduction

UseCases in React - Introduction

I have been struggling a lot with huge and complex React components. We usually write too much logic inside them. Also, we see no problem on mix this logic with third-party libraries. These practices along with local state management using hooks and our code is a growing mess.

I'm having a better time since I started to consistently use the UseCase layer on my applications. The way it is implemented in React is really simple. It's just an async function that returns an object containing success and error. The first time I show something like this being used in React was in this Paul Allies article. Since then I added one thing or two which I will show in this UseCase series.

The big advantage of this layer is the business logic isolation. We can uncouple flows containing rules with steps, dependencies and so on, and hide all these from our component (our Presentation layer). Our Presentation layer just needs to know if everything went fine (or not).

But talk is cheap, let's dive into the code.

The UseCaseReturnType

This is what a call to a UseCase looks like:

const { success, error } = await myUseCase()

Nothing fancy, right? Great, good code is simple code. Return success AND error is a convenience widely used in software engineering. I like to think that we are giving the same weight to both. There is no correct return. It's just what is possible. And we must (and this way we actually can) handle both responsibly.

Every UseCase has a UseCaseReturnType which I define like this:

type UseCaseSuccessType<T> = T | null

type UseCaseErrorType = ErrorType | null

type UseCaseReturnType<T> = Promise<{
  success: UseCaseSuccessType<T>
  error: UseCaseErrorType
}>

ErrorType is just a common type that I use across all the code. I type my UseCase according to what it will return if everything went well (success). For instance, to fetch a list of Products I normally would do:

const myUseCase = 
  async (): UseCaseReturnType<Products[]> => { ... }

This way I can:

  1. expect that any caller of this UseCase would know upfront what data will be returned and do something with it.

  2. be safe when I pass this UseCase to someone that is expecting a function (like a Component prop). If success types do not match, Typescript will warn me.

When something went wrong I receive error. It's up to you to choose what to do with it. What I normally do is render an ErrorModal which is shared across the application. This ErrorModal shows an error message and, based on where the error happens, it can offer some options to the user on what to do next: retry, navigate back or even nothing. My app does not crash, I give feedback and some fair options to the user.

Problems that UseCase target

  1. Scattered business rules throughout the codebase.

    I had a social auth flow that started on a presentation component, went through a container and finished in a context. This is evil. Related business code should live together or even be close to each other. For a flow that contains business rules, this is critical.

  2. No control over loading, error and refetch.

    I had some problems using Apollo hooks (useQuery, useMutation) inside my components. Besides the overloading and coupled code, they handle stuff internally. I can't fully control what is happening. When unexpected behavior happens, the option was to search for a workaround. So, I removed these hooks and wrap the apollo client instance inside my UseCases. Things start to work as expected, I decoupled the third-party libraries code from my code and my components look better.

  3. The presentation layer deals with more than presentation stuff

    I'm pretty sure that my presentation components, even the "ugly" ones, are dealing with things that belong to them. The local state management and the use of React hooks are just to handle the presentation logic. The complexity that lives there belongs there, so, is necessary.

  4. Several TryCatch blocks

    I always wondered where the TryCatch blocks should live on a front-end application. I found UseCases a good place for them. Async operations are out of my control, so they can go wrong often. I can give error feedback based on which UseCase failed. And I can handle it (the error) smoothly on my component with no need to crash or close the app.

What comes next?

In the next articles, I must target the questions:

  1. How does it look from the inside?

  2. How do I handle the result in my presentation component?

  3. How do I manage dependencies?

  4. Situations where I exchanged hooks for events

  5. Give back partial results to my presentation components

References

https://www.plainionist.net/Implementing-Clean-Architecture-UseCases/

https://paulallies.medium.com/clean-mvvm-with-react-and-react-hooks-ebc37b22542f

Clean Architecture, Chapter 20, Bussiness Rules