React|Architecture and Design

React Clean Architecture: A 3-Layer Separation Strategy (Domain, Data, Presentation)

6

Let's crack open Chris's project folder. Inside src/components, there are over 100 files piled up.

Some components call fetch directly, some manage state, and others just draw the UI.

"I need to fix the login logic, but where on earth is it?"

He spends the entire day just looking for the file. As the project grows, the code gets tangled like spaghetti, and fixing one feature causes an error in a completely unrelated place.

To quell this chaos, we need a rule that defines "where code should live"—in other words, an Architecture.

Today, we will learn how to reinterpret the concept of Clean Architecture, primarily used in backend development, into 3 Layers suitable for React development.

1. The 3-Layer Structure: Separation of Roles

We will divide the app into three major chunks.

  • Presentation Layer (UI): Concerns itself with how things look to the user. (Components, View)
  • Domain Layer (Business): Manages the core rules and state of the app. (Hooks, Store, Model)
  • Data Layer (Infrastructure): Communicates with the outside world (Server, DB). (API, DTO, Mapper)
  • The core rule of this hierarchy is "Dependencies only point downwards." The UI knows the Domain, the Domain knows the Data, but the Data layer must not know who is using it.

    2. Layer 1: Data Layer

    This is the deepest layer. Its only role is to communicate with external APIs and transform (Map) data into a format suitable for our app. Not a single line of UI logic should exist here.

  • Components: API call functions, DTO type definitions, Mapper functions.
  • typescript
    // src/features/user/api/userApi.ts (Data Layer)
    import { axiosInstance } from '@/shared/api';
    import { UserDTO, User } from '../model/types';
    import { userMapper } from '../lib/userMapper';
    
    // 1. Fetch external data
    export const getUser = async (id: number): Promise<User> => {
      const { data } = await axiosInstance.get<UserDTO>(`/users/${id}`);
      
      // 2. Transform into a format suitable for our app (Map)
      return userMapper(data);
    };

    3. Layer 2: Domain Layer (Business Layer)

    This acts as the brain of the app. Core logic like "Fetch user info and save to state" or "Calculate cart total" lives here. In React, Custom Hooks or Zustand Stores usually fulfill this role.

  • Components: Custom Hooks, Global Store, Utility Functions.
  • Characteristics: Calls Data Layer functions but knows nothing about what the UI looks like.
  • typescript
    // src/features/user/model/useUser.ts (Domain Layer)
    import { useQuery } from '@tanstack/react-query';
    import { getUser } from '../api/userApi'; // Uses Data Layer
    
    export const useUser = (userId: number) => {
      // 1. Manage data via Data Layer
      const { data, isLoading, isError } = useQuery({
        queryKey: ['user', userId],
        queryFn: () => getUser(userId),
      });
    
      // 2. Domain Logic (e.g., Determine if admin)
      const isAdmin = data?.role === 'ADMIN';
    
      // 3. Return what the UI needs in a convenient format
      return {
        user: data,
        isLoading,
        isError,
        isAdmin,
      };
    };

    4. Layer 3: Presentation Layer (UI Layer)

    This is the screen the user sees. This layer must be very "Dumb."

    It does not perform complex calculations or direct API calls. It simply receives data provided by the Domain Layer and draws it.

  • Components: React Components, CSS/Styles.
  • Characteristics: It only looks at the Domain Layer (Hooks).
  • typescript
    // src/features/user/ui/UserProfile.tsx (Presentation Layer)
    import { useUser } from '../model/useUser'; // Uses Domain Layer
    
    export function UserProfile({ userId }: { userId: number }) {
      // Delegate logic to the Hook
      const { user, isLoading, isAdmin } = useUser(userId);
    
      if (isLoading) return <div>Loading...</div>;
    
      return (
        <div className="card">
          <h1>{user?.name}</h1>
          {/* Only View logic exists */}
          {isAdmin && <span className="badge">Admin</span>}
        </div>
      );
    }

    5. Applying to Folder Structure (A Taste of Feature Sliced Design)

    We can organize these 3 layers into an actual folder structure. Grouping by Feature is excellent for maintainability.

    typescript
    src/
      features/
        user/           # Feature: User
          api/          # [Data] API, DTO
            userApi.ts
          model/        # [Domain] Type, Hook, Store
            types.ts
            useUser.ts
          ui/           # [Presentation] Components
            UserProfile.tsx
          index.ts      # Export only what needs to be public (Public API)

    Now Chris doesn't have to wonder where to put his code.

  • "API Call?" -> api/
  • "State Management?" -> model/
  • "Change Button Color?" -> ui/
  • 6. Benefits of Clean Architecture

  • Replaceability: What if we switch from REST API to GraphQL? You only need to swap the Data Layer. The UI and Business Logic don't need to be touched.
  • Testability: UI testing is hard, but logic testing is easy. You can isolate and test the Domain Layer separately.
  • Collaboration Efficiency: Publishers only work in the ui folder, and frontend developers work in model and api folders, reducing merge conflicts.
  • Key Takeaways

  • Presentation (UI): "The Showing." Receives data using hooks from the Domain layer and renders it.
  • Domain (Business): "The Working." Calls the Data layer and manages state and rules.
  • Data (Infra): "The Communicating." Talks to the server and transforms data (Mapper).
  • Dependency Direction: Presentation -> Domain -> Data. Backflow is strictly forbidden.

  • We've neatly sliced the code into layers. Now it's time to verify "if my code works correctly."

    Can we test the logic even before drawing the UI? Let's build a testing strategy that targets each layer using Vitest.

    Continuing in: “Testing by Layer with Vitest: What to Test and What to Give Up”

    🔗 References

  • The Clean Architecture (Robert C. Martin)
  • Feature Sliced Design
  • Bulletproof React - Project Structure
  • Comments (0)

    0/1000 characters
    Loading comments...