The Use Case pattern is nothing sort of new, it has been around longer than I've been alive, however, the way I apply in the Frontend is something that I haven't seen (yet). The Use Case pattern allows you to separate your business logic in such a way that you can add different pieces of code to completely change the way your code runs in an easy and transversal way.
“”The Use Case Pattern allows you to orchestrate your business logic calls.
What is a Use Case?
A Use Case represents how a user interacts with the system. Everything a user can do, well, that's a use case. When coding a use case, the idea is to encapsulate a complete set of actions, so that the use cases can be reused. Use cases orchestrate, they know who to call and integrate the different pieces together so we return to the UI what it needs. Let's see some code so we can get a better understanding.
The UseCase Interface
Everything starts with the UseCase interface:
export interface UseCase<In, Out> { handle(input?: In): Promise<Out> }
A UseCase is a class (it can be a function too, but I prefer classes) that has a method called handle(). This method, receives an input typed as In and return an output typed as Out.
Functions that are defined inside classes we call them methods.
As you might have noticed, the output returned is always a Promise. This is a conscious design choice given that most of the time in the Frontend, our use cases are going to indirectly make API calls, which happen to be asynchronous.
If you need to execute a computation-heavy use case, using Promises means you can also avoid blocking the main thread. Threads in JavaScript?! Yes, check it out hereOpen in a new tab.
The UseCase implementation
Now we can implement the UseCase interface.
export class GetDestinationsQry implements UseCase<void, Destination[]> { constructor( private readonly destinationRepository: DestinationRepository, private readonly destinationOrderer: DestinationOrderer, ) {} async handle(): Promise<Destination[]> { return this.destinationOrderer.order(await this.destinationRepository.findAll()) } }
Here we are using two classes:
DestinationRepository: Makes an API Request using the Repository Pattern (which I'll talk about in another newsletter)DestinationOrderer: Orders the Destination using a Domain class
Now, you ar probably asking yourself... Why would I add this complexity to my code? I could just call the API in the UI using fetch and order the Destinations in the component that makes the call.
Well, for starters, as you can see in this code, there's no UI code, which means I can in the future change UI frameworks or call this Use Case from different places without needing to change a lot of code. This has something to do with how I apply Software Architecture Layers and Domain Driven Design, which I'll explain in another issue of this Newsletter.
And I can't get the benefits of abstracting how I execute the use cases to handle transversal functionality. Which is what I'll explain next.
The UseCaseService
The trick is how to execute the UseCase. One would think you directly create the instance and execute them as follows, from let's say your ReactOpen in a new tab Component:
import { type FC } from 'react' const getDestinationsQry = new GetDestinationsQry(destinationRepository, destinationOrderer) export const Comp: FC = () => { return <button onClick={() => getDestinationsQry.handle()}>Click me</button> }
However, we are missing a great opportunity. What if we want to support the following features:
- Error handling
- Logging
- Cache
- And many more!
The way to do that is by creating the UseCaseService, which helps us abstract how a use case is executed.
export class UseCaseService { execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { return useCase.handle(params) as Promise<Out> } }
It doesn't look like much, however, this changes everything. What if we want to add logging to all the use cases? Well, instead of adding console.log here or there we can just do this:
export class UseCaseService { execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { console.log(useCase.constructor.name) console.log(params) return useCase.handle(params) as Promise<Out> } }
As easy as that!
In production, since frameworks tend to mangle class names to reduce the size of the code I usually add a static field called ID with the name of the class which I use for logging and for dependency injection.
Let's make that change!
export class GetDestinationsQry implements UseCase<void, Destination[]> { static readonly ID = 'GetDestinationsQry' ... } export class UseCaseService { execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { console.log(useCase.ID) console.log(params) return useCase.handle(params) as Promise<Out> } }
Interestingly enough we can also await the execution of the use case to log the result:
export class UseCaseService { async execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { console.log(useCase.ID) console.log(params) const result = await useCase.handle(params) as Promise<Out> console.log(result) return result } }
This is quite useful since we might want to execute code after executing use cases.
“”In another issue of the newsletter I'll show you how to improve the UseCaseService so as not to mix different logic in this class with Middlewares and the Chain of Responsibility PatternOpen in a new tab.
What about if I want to implement global error handling?
export class UseCaseService { async execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { try { console.log(useCase.ID) console.log(params) const result = await useCase.handle(params) as Promise<Out> console.log(result) return result } catch (e) { alert(e) throw e } } }
Now I have a way to display to the user the errors in a generic way! There are endless possibilities.
Let me show you how to use the UseCaseService
Executing the UseCaseService
import { type FC } from 'react' const useCaseService = new UseCaseService() const getDestinationsQry = new GetDestinationsQry(destinationRepository, destinationOrderer) export const Comp: FC = () => { return <button onClick={() => useCaseService.execute(getDestinationsQry)}>Click me</button> }
Usually to avoid creating instances here and there I have a container class that handles the instantiation of the classes, and then, I either just import the instances or I use a Dependency Injection ContainerOpen in a new tab to retrieve the instance.
Here is a simplified example:
export const destinationApiRepository = new DestinationApiRepository() export const destinationOrderer = new DestinationOrderer() export const getDestinationsQry = new GetDestinationsQry(destinationApiRepository, destinationOrderer) export const useCaseService = new UseCaseService()
Calling UseCaseService from the UI
Since I also want to probably call the use case from the UI and handle loading, I create a last wrapper that handles that for me. Let me show you an example in React:
export interface UseUseCaseOptions<In> {} /** * State returned by the useUseCase hook. */ export interface UseCaseState<In, Out> { /** Whether the use case is currently executing */ isLoading: boolean /** Function to execute the use case */ execute: (params?: In) => Promise<Out> } function useUseCase<In, Out>(useCase: UseCase<In, Out>, options: UseUseCaseOptions<In> = {}): UseCaseState<In, Out> { const [isLoading, setIsLoading] = useState(false) const execute = useCallback( async (params?: In): Promise<Out> => { setIsLoading(true) try { const result = await useCaseService.execute(useCase, params) return result } finally { setIsLoading(false) } }, [useCase], ) return { isLoading, execute, } }
Now, in the component you can call the hook like this:
import { type FC } from 'react' import { getDestinationsQry } from './container' import { useUseCase } from './use-use-case' export const Comp: FC = () => { const { execute, isLoading } = useUseCase(getDestinationsQry) return <button disabled={isLoading} onClick={() => execute()}>Click me</button> }
Conclusion
Use Cases provide 3 main benefits in Frontend Architecture:
- Provide an extension point to add transversal functionality.
- Makes you think about how the user interacts with your application abstracting their actions into code.
- Allow you to easily reuse use cases or compose bigger use cases calling smaller ones.
With that, I hope you enjoyed the very first issue of this newsletter! If you find it useful, please share it with others, it makes my day!
Feel free to reply to this email if you've found any typos or the email is not displayed correctly, I'll make sure to fix it for the web version and take note for the next newsletter. Thank you very much for reading here!
P.S: Want to learn more about use cases? Read another post about use casesOpen in a new tab on my web.