This article shares the concept and implementation of the usecase unit in frontend applications within the Clean Architecture.

Repository with example:
https://github.com/harunou/react-tanstack-react-query-clean-architecture

The usecase is a unit that orchestrates the application by coordinating entities, gateways, and transactions to fulfill specific user goals. It implements application business rules.

The usecase unit does not return any data because, according to unified data flow, the data flows from the usecase unit into the view unit through entities, gateways, and selectors.

Usecase Implementation

The usecase implements an interface provided by a consumer. The interface could be just a function which the consumeru should provide or a more complex one used globally across the application.

The unit has two possible implementation types: inline and extracted. In practice, the unit evolves in the following way:

------------------      ---------------------
| inline usecase | ---> | extracted usecase |
------------------      ---------------------

Any usecase implementation starts from a simple inline function in a consumer (controller).

All development context is focused on the application orchestration logic only.

Inline Usecase Implementation

Let's look at a basic example, where we have already implemented the view and controller.

interface OrderProps {
  orderId: string;
}

interface Controller {
  deleteOrderButtonClicked(id: string): Promise<void>;
}

const useController = (params: { orderId: string }): Controller => {
  const deleteOrderButtonClicked = async () => {};

  return { deleteOrderButtonClicked };
};

export const Order: FC<OrderProps> = (props) => {
  const presenter = usePresenter(props);
  const controller = useController(props);

  return (
    <>
      <button onClick={controller.deleteOrderButtonClicked}>Delete</button>
      <div style={{ padding: "5px" }}>
        {presenter.itemIds.map((itemId) => (
          <OrderItem key={itemId} itemId={itemId} />
        ))}
      </div>
    </>
  );
};

The usecase we need should delete an order in a remote source and reset the items filter. Observing the codebase, we found that filter values are stored in the store and remote orders can be deleted with a mutation call. Then implementation of inline usecase could look next:

interface OrderProps {
  orderId: string;
}

interface Controller {
  deleteOrderButtonClicked(id: string): Promise<void>;
}

const useController = (params: { orderId: string }): Controller => {
  const setItemsFilterById = useOrdersPresentationStore((state) => state.setItemsFilterById);

  const resource = useOrdersResourceSelector();
  const { mutateAsync: deleteOrder } = useMutation({ ...useDeleteOrderOptions(resource) });

  const deleteOrderButtonClicked = async () => {
    // inline usecase
    await deleteOrder({ id: params.orderId });
    setItemsFilterById(null); // pessimistic update
  };

  return { deleteOrderButtonClicked };
};

export const Order: FC<OrderProps> = (props) => {
  const presenter = usePresenter(props);
  const controller = useController(props);

  return (
    <>
      <button onClick={controller.deleteOrderButtonClicked}>Delete</button>
      <div style={{ padding: "5px" }}>
        {presenter.itemIds.map((itemId) => (
          <OrderItem key={itemId} itemId={itemId} />
        ))}
      </div>
    </>
  );
};

Extracted Usecase Implementation

The final step is to observe the codebase for the need of usecase extraction and reuse. The extraction happens if any other consumer unit already has the same logic implemented or the usecase becomes more complex. In this case, the inline usecase evolves to an extracted usecase.

interface OrderProps {
  orderId: string;
}

interface Controller {
  deleteOrderButtonClicked(id: string): Promise<void>;
}

// extracted usecase
export const useDeleteOrderUseCase = (): { execute: (params: { orderId: string }) => Promise<void> } => {
  const setItemsFilterById = useOrdersPresentationStore((state) => state.setItemsFilterById);
  const resource = useOrdersResourceSelector();
  const { mutateAsync: deleteOrder } = useMutation({ ...useDeleteOrderOptions(resource) });

  const execute = async (params: { orderId: string }) => {
    await deleteOrder({ id: params.orderId });
    setItemsFilterById(null); // pessimistic update
  };

  return { execute };
};

const useController = (params: { orderId: string }): Controller => {
  const { execute: executeDeleteOrderUseCase } = useDeleteOrderUseCase();

  const deleteOrderButtonClicked = async () => {
    await executeDeleteOrderUseCase({ orderId: params.orderId });
  };

  return { deleteOrderButtonClicked };
};

export const Order: FC<OrderProps> = (props) => {
  const presenter = usePresenter(props);
  const controller = useController(props);

  return (
    <>
      <button onClick={controller.deleteOrderButtonClicked}>Deletebutton>
      <div style={{ padding: "5px" }}>
        {presenter.itemIds.map((itemId) => (
          <OrderItem key={itemId} itemId={itemId} />
        ))}
      div>
    >
  );
};

Naming of an extracted usecase is suggested to be based on the logic it performs followed by the suffix UseCase.

At this stage the usecase unit implementation is considered complete.

Q&A

How to test the usecase?

Usecase units can be tested both in integration with other units they depend on and in isolation by mocking dependencies. An example can be found here:
useDeleteOrderUseCase.spec.tsx

Where to place it?

Usecases are suggested to be placed in the usecases directory.

What is the usecase interface and do I need a specific one?

The minimal requirement for a usecase type is a function which does not return anything, because control flow goes reactively to the view unit through the store unit. As practice shows, it's best to have a globally defined usecase type
where the function returns Promise.

export type UseCase<T = void> = {
  execute: (params: T) => Promise<void>;
};

export const useDeleteOrderUseCase = (): UseCase<{ orderId: string }> => {
  const setItemsFilterById = useOrdersPresentationStore((state) => state.setItemsFilterById);
  const resource = useOrdersResourceSelector();
  const { mutateAsync: deleteOrder } = useMutation({ ...useDeleteOrderOptions(resource) });

  const execute = async (params: { orderId: string }) => {
    await deleteOrder({ id: params.orderId });
    setItemsFilterById(null);
  };

  return { execute };
};

const useController = (params: { orderId: string }): Controller => {
  const { execute: executeDeleteOrderUseCase } = useDeleteOrderUseCase();

  const deleteOrderButtonClicked = async () => {
    await executeDeleteOrderUseCase({ orderId: params.orderId });
  };

  return { deleteOrderButtonClicked };
};

Can I use a usecase inside another usecase?

No, usecases should not depend on each other because it will create a complex dependency graph and make the codebase hard to maintain.

Can I use multiple usecases at once?

Yes, multiple usecases can be used at once. It should be noted that usecase execution could result in async state change in the store unit, which result in subsequent intermediate re-render of the view unit. If such intermediate re-renders are not acceptable, consider implementing a custom usecase that reflects the current need.

const useController = (params: { orderId: string }): Controller => {
  // useDeleteOrderUseCase is a usecase that pessimistically deletes order
  // triggering update of the store unit and the view unit rerender
  const { execute: executeDeleteOrderUseCase } = useDeleteOrderUseCase();
  // useUpdateOrdersUseCase is a usecase that updates orders of the store unit
  // and the view unit rerender
  const { execute: executeUpdateOrdersUseCase } = useUpdateOrdersUseCase();

  const deleteOrderButtonClicked = async () => {
    await executeDeleteOrderUseCase({ orderId: params.orderId });
    // rerender of view unit
    await executeUpdateOrdersUseCase();
    // rerender of view unit
  };

  return { deleteOrderButtonClicked };
};

Conclusion

The usecase unit is one of the most important and powerful tools that orchestrates the application. The unit encapsulates the application business logic. Starting with inline usecases and extract them only when necessary, allowing your architecture to evolve naturally with your application's complexity.