This article shares the concept and implementation of the Effect unit in frontend applications within the Clean Architecture.
Repository with example:
https://github.com/harunou/react-tanstack-react-query-clean-architecture
The Effect unit encapsulates logic interacting with external resources through gateways. It manages side effects, asynchronous operations, and request orchestration.
Effect units handle cross-gateway requests, data fetching, sending processes, and sharing fetch logic across multiple use case interactors.
Effect Implementation
An Effect implements an interface provided by a consumer (use case interactor). This interface could represent a function required by use case or more complex one, which is use globally across application.
Effect implementations evolve from an inline form within consumers to extracted units:
--------------- ------------------
| inline effect | ---> | extracted effect |
--------------- ------------------
Initially, effects are implemented inline, focused on fetching data, sending requests, and orchestrating interactions.
Inline Effect Implementation
Consider a use case interactor that initializes an entities store upon application startup, requiring orders and users data:
export const useInitializeApplicationUseCase = (): { execute: () => Promise<void> } => {
const usersGateway = useUsersGateway();
const ordersGateway = useOrdersGateway();
const execute = async () => {
// inline effect
const users = await usersGateway.getUsers();
const orders = await ordersGateway.getOrders();
setUsersAndOrdersTransaction({ users, orders });
};
return { execute };
};
Extracted Effect Implementation
The inline implementation can be extracted into a reusable effect unit:
// File: getUsersAndOrdersEffect.ts
export const getUsersAndOrdersEffect = async () => {
const usersGateway = useUsersGateway();
const ordersGateway = useOrdersGateway();
const users = await usersGateway.getUsers();
const orders = await ordersGateway.getOrders();
return { users, orders };
};
// File: useInitializeApplicationUseCase.ts
export const useInitializeApplicationUseCase = (): { execute: () => Promise<void> } => {
const execute = async () => {
const { users, orders } = await getUsersAndOrdersEffect();
setUsersAndOrdersTransaction({ users, orders });
};
return { execute };
};
Extracted effects encapsulate logic completely, allowing internal modifications such as parallel requests without affecting consumers:
// File: getUsersAndOrdersEffect.ts
export const getUsersAndOrdersEffect = async () => {
const usersGateway = useUsersGateway();
const ordersGateway = useOrdersGateway();
// Fetch users and orders in parallel
const [users, orders] = await Promise.all([
usersGateway.getUsers(),
ordersGateway.getOrders(),
]);
return { users, orders };
};
// File: useInitializeApplicationUseCase.ts
export const useInitializeApplicationUseCase = (): { execute: () => Promise<void> } => {
const execute = async () => {
const { users, orders } = await getUsersAndOrdersEffect();
setUsersAndOrdersTransaction({ users, orders });
};
return { execute };
};
Effect names should clearly indicate their function and end with the suffix Effect
.
Q&A
How should an Effect unit be tested?
Effect units can be tested in integration with their dependent units or in isolation using mocks.
Where should Effects be placed?
Effects are suggested to be placed in a dedicated effects
directory.
Can Effects be nested?
Yes, Effects can be nested. However, nesting may complicate reasoning and testing, so use with care.
Can multiple Effects be used simultaneously?
Yes, using multiple effects simultaneously is common and practical.
Conclusion
Effect units effectively manage asynchronous operations and coordinate cross-gateway interactions. Begin with inline implementations and extract effects only as needed, maintaining architecture flexibility as application complexity grows.
Top comments (0)