DEV Community

Dzmitry Harunou
Dzmitry Harunou

Posted on • Edited on

View Unit in Clean Architecture for Frontend Applications

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

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

Table of Contents

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

View Visualization

The image demonstrates a component diagram and its elements and dependencies.

component diagram

As mentioned in the overview, the view depends on presenter and controller through provided interfaces. The controller and presenter implement these interfaces and depend on the application core. Additionally, the controller and presenter could depend on props or/and the component's local state whenever needed.

The control flow and data flow are straightforward:

component flows

Data adapted for presentation flows from the presenter into the view, while the controller handles user events captured by the view.

View Implementation

View 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 store 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 view improvements or updates should also remain within the boundaries of layout and interfaces.

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

Q&A

How to test the view?

A view can be tested manually because it can be rendered in a browser with mock data. Additionally, the view 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 view. If the view is simple, where values and handlers can be easily observed, then it's not necessary. The interfaces summarize the data and actions that the view 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 view 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 view 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

Next example demonstrates same view with presenter interface. The constant presenter represents 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;
}
t
// 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 view is too complex and should be split into smaller, more specific parts. Interface declarations can help identify how a component can be decomposed.

Conclusion

View unit serves as the boundary between users and the system. When working with the view, the development context is limited to component layout and interfaces, reflecting the view'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 view structure, but switching between approaches is not a big deal.

Top comments (0)