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.
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:
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;
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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>
);
};
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)