Clean Architecture in Frontend Applications. Entities Store
This article shares the concept and implementation of the entities store unit in frontend applications within the Clean Architecture.
Repository with example:
https://github.com/harunou/react-tanstack-react-query-clean-architecture
The store unit maintains a collection of enterprise business entities and/or application business entities and their states. An entity can be an object with methods, or it can be a set of data structures and functions. It depends on the the way the store implemented and external libraries used.
Store Implementation
Entities store implementation starts from modeling entities, which should keep minimal data needed to make the application working properly. Basis for the entities store models could be view unit's public interfaces, business logic requirements, etc. The important rule is that the entities store should serve the frontend application purpose and not more.
Particular store implementation depends on a library used (React useState, useReducer, Redux, Mobx, Zustand, etc.).
The unit has two possible implementation types: inline
and extracted
. In practice, the unit evolves in the following way:
-------------- -----------------
| inline store | ---> | extracted store |
-------------- -----------------
Additionally, both inline and extracted stores can be split into domain and presentation stores. The domain store manages enterprise business entities, while the presentation store handles application business entities.
All development context is focused on modeling entities.
Inline entities store implementation
Let's look at a basic example, where we have already implemented inlined business and application entities stores, use case, presenter, controller and view units.
interface ItemEntity {
id: string;
productId: string;
quantity: number;
}
interface OrderEntity {
id: string;
userId: string;
items: ItemEntity[];
}
export const TotalOrdersAndItemsAmount: FC = () => {
// inline enterprise business entities store
const [orders, setOrders] = useState<OrderEntity[]>([]);
// inline application business entity store
const [isLoading, setIsLoading] = useState(false);
const ordersGateway = useOrdersGateway();
// inline use case interactor
const loadOrders = async () => {
setIsLoading(true);
const orders = await ordersGateway.getOrders();
setOrders(orders);
setIsLoading(false);
};
// inline presenter
const totalOrdersAmount = orders.length;
const totalItemsAmount = orders.reduce((acc, order) => acc + order.items.length, 0);
// inline controller
const refreshButtonClicked = () => {
loadOrders();
};
const componentMounted = () => {
loadOrders();
};
// lifecycle hook
useEffect(() => {
componentMounted();
}, []);
// view
return (
<>
<button onClick={refreshButtonClicked}>Refresh</button>
{isLoading && <div>Loading....</div>}
<div>Total orders Amount: {totalOrdersAmount}</div>
<div>Total items Amount: {totalItemsAmount}</div>
</>
);
};
Extracted entities store implementation
When the state needs to be shared across multiple components, extracting it into a centralized store is suggested. Various approaches and libraries can be used to manage the extracted state. The following example demonstrates an implementation using the Zustand state manager.
// File: OrdersStore.tx
export interface ItemEntity {
id: string;
productId: string;
quantity: number;
}
export interface OrderEntity {
id: string;
userId: string;
items: ItemEntity[];
}
type OrdersStore = {
orders: OrderEntity[];
setOrders: (orders: OrderEntity[]) => void;
};
export const useOrdersStore = create<OrdersStore>((set) => ({
orders: [],
setOrders: (orders) => set({ orders }),
}));
// File: TotalOrdersAndItemsAmount.tsx
export const TotalOrdersAndItemsAmount: FC = () => {
// extracted enterprise business entities store
const orders = useOrdersStore((state) => state.orders);
const setOrders = useOrdersStore((state) => state.setOrders);
// inline application business entity store
const [isLoading, setIsLoading] = useState(false);
const ordersGateway = useOrdersGateway();
// inline use case interactor
const loadOrders = async () => {
setIsLoading(true);
const orders = await ordersGateway.getOrders();
setOrders(orders);
setIsLoading(false);
};
// inline presenter
const totalOrdersAmount = orders.length;
const totalItemsAmount = orders.reduce((acc, order) => acc + order.items.length, 0);
// inline controller
const refreshButtonClicked = () => {
loadOrders();
};
const componentMounted = () => {
loadOrders();
};
// lifecycle hook
useEffect(() => {
componentMounted();
}, []);
// view
return (
<>
<button onClick={refreshButtonClicked}>Refresh</button>
{isLoading && <div>Loading....</div>}
<div>Total orders Amount: {totalOrdersAmount}</div>
<div>Total items Amount: {totalItemsAmount}</div>
</>
);
};
Q&A?
How to test the store?
The entities store unit can be tested both in integration with other units and in isolation, following the guidelines provided by the store library provider.
Do I need to split enterprise (domain) and application (presentation) business entities store?
As practice shows, it is worth splitting them. In the beginning, the values can live together, but as the store grows, it is recommended to separate these types of entities. Keeping enterprise business entities separate makes it easier to observe and understand the core data necessary for the application to work.
Conclusion
Entities store unit helps organize and manage both enterprise (domain) and application (presentation) business entities, supporting a clear separation of concerns. Starting with an inline store is often sufficient for simple cases, but as the application grows, extracting the store and splitting domain and presentation entities becomes beneficial. Following these architectural principles leads to robust frontend applications.
Top comments (0)