common-mistakes

UI Components Doing Too Much

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

One of the common mistakes in frontend development is the failure to separate business logic from component logic. Components should primarily focus on the user interface. They should only contain simple code and logic related to data display and state management.

Although a component may need to make an API call on initial render to fetch the necessary information, it's better to abstract the API call logic by moving the interaction to a separate service. This provides several key benefits:

  • Task Separation: By separating business logic and component logic, you simplify each one. Components deal exclusively with display and user input handling, while business logic handles data processing and API interactions. This also makes it easier to test components since each can be tested in isolation from the business logic.

  • Reusability: Business logic, when moved to separate modules, can be easily reused in other parts of the application. This reduces code duplication and makes it easier to maintain. Additionally, it simplifies making changes to the business logic without altering the UI components.

UI components can become quite complex due to the logic needed to display various parts of the interface. Adding business logic makes the component even more complicated, making the code harder to understand and maintain.

An example of the correct approach is using React Hooks or contexts to manage state and effects within functional components while moving business logic to separate functions or classes. For instance, useEffect can be used to perform side effects like API calls, and useState to manage the component's local state.

Example

Let's consider a simple example of a component that should display a list of users fetched from a server using a fetch request. First, let's take a look at the version with non-separated business logic. Here, the component is responsible not only for displaying the list of users but also for making the API call:

Code implementation
1const UsersList: FC = () => {
2 const [users, setUsers] = useState<IUsers>([]);
3
4 useEffect(() => {
5 const fetchUsers = async () => {
6 const response = await fetch(
7 'https://jsonplaceholder.typicode.com/users'
8 );
9
10 if (!response.ok) {
11 console.log('Failed to fetch users');
12 // In this example it just logs the error to the console,
13 // additional error handling logic can be added here as needed
14 }
15 const data: IUsers = await response.json();
16
17 setUsers(data);
18 };
19
20 fetchUsers();
21 }, []);
22
23 return (
24 <ul>
25 {users.map((user) => (
26 <li key={user.id}>{user.name}</li>
27 ))}
28 </ul>
29 );
30};
31

To improve the code structure, we will move the data-fetching logic (API call) to a separate service module, the userService.ts file:

Code implementation
1const fetchUsers = async (): Promise<IUsers> => {
2 const response = await fetch('https://jsonplaceholder.typicode.com/users');
3
4 if (!response.ok) {
5 console.log('Failed to fetch users');
6 // In this example it just logs the error to the console,
7 // additional error handling logic can be added here as needed
8 }
9 return await response.json();
10};
11

Now our familiar component uses the logic moved to the service to fetch the data:

Code implementation
1const UsersList: FC = () => {
2 const [users, setUsers] = useState<IUsers>([]);
3
4 useEffect(() => {
5 const getUsers = async () => {
6 const data = await fetchUsers();
7
8 setUsers(data);
9 };
10
11 getUsers();
12 }, []);
13
14 return (
15 <ul>
16 {users.map((user) => (
17 <li key={user.id}>{user.name}</li>
18 ))}
19 </ul>
20 );
21};
22

In this improved example, the component focuses on displaying the data and managing its own state, while all business logic for fetching data is moved to a separate service. As the saying goes, each of them now minds its own business.

This approach helps make the code cleaner, more modular, and easier to maintain. It simplifies testing, reuse, and code changes. By following these principle, you can significantly improve the quality and maintainability of your frontend application.

Last updated on by @skrykateSuggest an improvement on Github repository.