Clean Architecture in Front-End using React: A Practical Guide

Published on 4 de setembro de 2025
Clean Architecture in Front-End using React: A Practical Guide

Clean Architecture is a design philosophy that emphasizes separation of concerns and independence from frameworks. While it's widely adopted in backend systems, it's often overlooked in front-end development. This post aims to bridge that gap and show you how to apply Clean Architecture principles to React applications in a practical, scalable way.

What Is Clean Architecture?

Clean Architecture, popularized by Robert C. Martin (Uncle Bob), is a way of structuring code to make systems independent of frameworks, UI, databases, and external agencies. It emphasizes:

  • Separation of concerns
  • Independence of frameworks
  • Testability and maintainability
  • Dependency inversion (inner layers shouldn’t depend on outer ones)

At its core, Clean Architecture splits your app into layers:

  1. Domain (business rules)
  2. Application (use cases)
  3. Infrastructure (frameworks, tools, APIs)
  4. UI (React components)

Why Clean Architecture for React?

React is flexible but unopinionated. This often leads to messy, tightly coupled code where business logic lives inside components, testing is painful, and changes ripple through the app.

Problems it solves:

  • Spaghetti code in components
  • Untestable logic tied to the DOM
  • Hard-to-reuse business logic
  • Difficult onboarding for new developers

Clean Architecture brings order. It gives each piece of logic a clear home, and makes your codebase easier to understand, test, and grow.


The Layers Explained

Domain Layer

This is the heart of your app. It contains Entities and Value Objects, which encapsulate core business logic without any dependency on React or APIs.

Example: User.ts

export class User {
  constructor(
    public readonly id: string,
    public readonly name: string
  ) {
    if (!name) throw new Error("User name cannot be empty");
  }

  rename(newName: string) {
    if (!newName) throw new Error("New name cannot be empty");
    return new User(this.id, newName);
  }
}

Application Layer

This is where use cases live. It orchestrates domain logic and defines interfaces for dependencies.

Example: fetchUserProfile.ts

import { User } from "../domain/User";

export interface UserRepository {
  getUserById(id: string): Promise<User>;
}

export const fetchUserProfile = async (
  userId: string,
  repo: UserRepository
): Promise<User> => {
  return repo.getUserById(userId);
};

Infrastructure Layer

Implements interfaces defined in the application layer. Talks to APIs, local storage, etc.

Example: ApiUserRepository.ts

import { UserRepository } from "../application/fetchUserProfile";
import { User } from "../domain/User";

export class ApiUserRepository implements UserRepository {
  async getUserById(id: string): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    const data = await res.json();
    return new User(data.id, data.name);
  }
}

UI Layer (React)

React components live here. They call use cases via custom hooks and present data to the user. Keep components dumb and push logic elsewhere.

Example: useUserProfile.ts

import { useEffect, useState } from "react";
import { fetchUserProfile } from "../application/fetchUserProfile";
import { ApiUserRepository } from "../infrastructure/ApiUserRepository";

export const useUserProfile = (userId: string) => {
  const [user, setUser] = useState(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUserProfile(userId, new ApiUserRepository())
      .then(setUser)
      .catch(err => setError(err.message));
  }, [userId]);

  return { user, error };
};

Example: UserProfile.tsx

import React from "react";
import { useUserProfile } from "../hooks/useUserProfile";

export const UserProfile = ({ userId }: { userId: string }) => {
  const { user, error } = useUserProfile(userId);

  if (error) return <p>Error: {error}</p>;
  if (!user) return <p>Loading...</p>;

  return <h1>Hello, {user.name}!</h1>;
};

Cross-Cutting Concerns

Things like:

  • Logging
  • Error handling
  • Validation
  • Analytics

Should live in the infrastructure layer, not in your domain or UI.


Testing Strategy

  • Entities/Use Cases: Pure unit tests
  • Repositories: Integration tests
  • Hooks/Adapters: Integration or unit
  • UI Components: React Testing Library
  • API Contracts: Use tools like Pact

Project Structure

src/
  domain/
    User.ts
  application/
    fetchUserProfile.ts
  infrastructure/
    ApiUserRepository.ts
  ui/
    components/
      UserProfile.tsx
    hooks/
      useUserProfile.ts

By Layer vs By Feature + Layer

By layer is clean and easier to understand. By feature + layer scales better in large apps:

src/
  user/
    domain/User.ts
    application/fetchUserProfile.ts
    infrastructure/ApiUserRepository.ts
    ui/UserProfile.tsx
    ui/useUserProfile.ts

Practical Patterns

  • Repository Pattern for abstracting data sources
  • Adapter Pattern for transforming infrastructure data into domain objects
  • Factory Functions for injecting dependencies

Example:

// createUserProfileHook.ts
export const createUserProfileHook = (repo: UserRepository) => {
  return (userId: string) => {
    // return useUserProfile logic with injected repo
  };
};

Advanced Topics

  • DDD (Domain-Driven Design) in front-end: aggregate roots, value objects, etc.
  • CQRS: Separate commands and queries in state management
  • Event sourcing with tools like XState
  • Monorepos: Share domain and application layers across projects

Conclusion

Clean Architecture isn’t just for the backend. Bringing its principles into React makes your front-end code easier to test, reason about, and scale. You don’t have to refactor everything at once. Start small:

  • Extract one use case
  • Isolate one domain entity
  • Add a repository interface

Further Reading

  • "Clean Architecture" by Robert C. Martin
  • "Frontend Clean Architecture" by Ruben Verborgh (talk)
  • Kent C. Dodds on "UI as a function of state"

Want a cleaner, more scalable React codebase? Start applying Clean Architecture today.

#react#best practices#clean architecture