DEV Community

Dzmitry Harunou
Dzmitry Harunou

Posted on • Edited on

User Interface Unit in Clean Architecture for Frontend Applications

User Interface Unit in Clean Architecture

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

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

Table of Contents

User interface unit is typically a layout. For example, in a React component, the layout is represented with JSX. The role of the user interface is to present the application's data to the user and handle user input. The user interface is tightly coupled with the presenter and controller interfaces, which summarize the data and event handlers required for the user interface.

User Interface Implementation

User interface implementation is pretty formal and starts from a visual design. The design is translated into a layout. Development context is limited to everything related to visual representation: layout structure, styles, animations, design system, component library, accessibility, etc. There is no need to guess about a entities or external resources - this will be addressed later. The end result consists of three elements: the component layout, the controller interface and the presenter interface where it is necessary.

Let's look at an example.

At the very beginning, the layout is empty; the component returns null, not JSX. The presenter and controller interfaces are empty as well.

interface Presenter {
}
interface Controller {
}
export const Order: FC = () => {
  const presenter: Presenter = {};
  const controller: Controller = {};
  return null;
};
Enter fullscreen mode Exit fullscreen mode

Step by step, the layout is built, and the interfaces are gradually filled with properties. The naming convention for properties in the presenter and controller interfaces follows a simple pattern: presenter property names reflect the data that needs to be presented, while controller property names are event-based and clearly reflect the actions that need to be handled by the component.

interface Presenter {
  hasOrder: boolean;
  orderDate: string;
  userName: string;
}
interface Controller {
  deleteOrderButtonClicked: () => void;
}
export const Order: FC = () => {
  // presenter implementation with mock data
  const presenter: Presenter = {
    hasOrder: true,
    orderDate: "09-03-2025",
    userName: "John Doe",
  };

  // controller implementation with mock data
  const controller: Controller = {
    deleteOrderButtonClicked: () => {},
  };
  if (!presenter.hasOrder) {
    return null;
  }
  return (
    <div style={{ border: "1px solid red" }}>
      <button onClick={controller.deleteOrderButtonClicked}>
        Delete Order
      </button>
      <div>Date: {presenter.orderDate}</div>
      <div>User name: {presenter.userName}</div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Any further user interface improvements or updates should also remain within the boundaries of layout and interfaces.

At the point when the user interface meets all visual requirements and has controller and presenter interfaces defined, the user interface implementation can be considered complete. The next possible step is to create controller and presenter.

Q&A

How to test the user interface?

A user interface can be tested manually because it can be rendered in a browser with mock data. Additionally, the user interface can be tested by creating unit tests with mocked controller and presenter interfaces. An example can be found here: Orders.spec.tsx

Do I need to create presenter and controller interfaces for each component?

No, it depends on the complexity of the user interface. If the user interface is simple, where values and handlers can be easily observed, then it's not necessary. The interfaces summarize the data and actions that the user interface needs to present and handle. If you can build such a summary in your head without an interface declaration, the declaration can be omitted.

Understanding null interface

If a user interface is simple, it may not need to declare explicit interfaces and can instead rely on "null presenter" or "null controller" interfaces.

The example below demonstrates a user interface with a null presenter interface. The constants hasOrder, orderDate, and userName act as properties of the null presenter implementation and are assigned mock data.

export const Order: FC = () => {
  // Constants representing the null presenter implementation
  const hasOrder = true;
  const orderDate = "09-03-2025";
  const userName = "John Doe";
  if (!hasOrder) {
    return null;
  }
  return (
    <div style={{ padding: "5px" }}>
      <div>Date: {orderDate}</div>
      <div>User name: {userName}</div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

The next example demonstrates the same user interface with a presenter interface. The constant presenter represents the presenter assigned with mock data.

interface Presenter {
  hasOrder: boolean;
  orderDate: string;
  userName: string;
}
export const Order: FC = () => {
  const presenter: Presenter = {
    hasOrder: true,
    orderDate: "09-03-2025",
    userName: "John Doe",
  };
  if (!presenter.hasOrder) {
    return null;
  }
  return (
    <div style={{ padding: "5px" }}>
      <div>Date: {presenter.orderDate}</div>
      <div>User name: {presenter.userName}</div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Where should I place presenter and controller interfaces?

Once declared, the interfaces can be placed either in the same file as the component if they are implemented within that file:

// file:./Order.tsx
interface Presenter {
  hasOrder: boolean;
  orderDate: string;
  userName: string;
}
export const Order: FC = () => {
  const presenter: Presenter = {
    hasOrder: true,
    orderDate: "09-03-2025",
    userName: "John Doe",
  };
  if (!presenter.hasOrder) {
    return null;
  }
  return (
    <div style={{ border: "1px solid red" }}>
      <div>Date: {presenter.orderDate}</div>
      <div>User name: {presenter.userName}</div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Alternatively, they can be placed in a separate file if the interfaces are implemented in a different file as well:

// file:./Order.types.ts
export interface Presenter {
  hasOrder: boolean;
  orderDate: string;
  userName: string;
}

// file:./presenter.ts
import type { Presenter } from "./Order.types.ts"
export const presenter: Presenter = {
  hasOrder: true,
  orderDate: "09-03-2025",
  userName: "John Doe",
}

// file:./Order.tsx
import { presenter } from "./presenter";
export const Order: FC = () => {
  if (!presenter.hasOrder) {
    return null;
  }
  return (
    <div style={{ border: "1px solid red" }}>
      <div>Date: {presenter.orderDate}</div>
      <div>User name: {presenter.userName}</div>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

How can interfaces help improve component design?

Following the Interface Segregation Principle (ISP), large interfaces may indicate that a user interface is too complex and should be split into smaller, more specific parts. Interface declarations can help identify how a component can be decomposed.

Conclusion

User interface unit serves as the boundary between users and the system. When working with the user interface, the development context is limited to component layout and interfaces, reflecting the user interface's actual needs. The decision of whether to declare explicit presenter/controller interfaces should be guided by the component's complexity. The chosen consumption approach (props or presenter/controller implementations) can affect the number of props, properties in interfaces, and the user interface structure, but switching between approaches is not a big deal.

Top comments (0)