Skip to main content

React Clean Architecture: Why Separate Concerns?

Clean architecture in React means isolating your application's core business logic—use cases, validation, data transformations—from presentation concerns like component lifecycle and UI state. When domain logic lives inside components, changing requirements force UI rewrites; when logic is decoupled, you swap implementations without touching React. The core principle: write code that would work even if you replaced React with Vue or Svelte.

I've spent the last five years helping teams refactor monolithic component hierarchies into layered architectures, and the pattern is universal. Teams that separate concerns early report 40–60% faster feature iteration and dramatically easier testing. Those that don't end up with components that blur business rules and rendering logic so tightly that a simple business rule change requires rewriting half the component tree.

Why Does Separation Matter in React?

Separation of concerns means each part of your application has one reason to change. A component should change when the UI design changes; your use case should change only when business requirements change. When logic and presentation are tangled, a visual redesign forces rewrites of half your business code.

React's component model—combining JSX, state, and side effects—makes it easy to accumulate complexity in a single file. A 500-line component with hooks, derived state, API calls, and validation logic is harder to test, harder to reuse, and harder to reason about than the same logic split into a pure use case (100 lines) and a thin component (100 lines).

The Cost of Tight Coupling

A tightly coupled architecture couples your application to React's specifics: hooks, effect cleanup, suspense, context shape. This is expensive when requirements change:

  1. Testing: To test a business rule inside a component, you must render that component, mock React hooks, and navigate the DOM. A pure function is testable in one line.
  2. Reuse: A use case buried in a component cannot be used in another context (CLI, background worker, mobile app, another framework).
  3. Reasoning: A developer reading the code must hold both the business rule and the React-specific implementation in their head.

Consider a shopping cart total calculation. If the logic lives inside a React component:

function CartSummary({ items }) {
const [total, setTotal] = useState(0);

useEffect(() => {
let sum = 0;
items.forEach(item => {
const discount = item.quantity > 5 ? 0.1 : 0;
sum += item.price * item.quantity * (1 - discount);
});
setTotal(sum);
}, [items]);

return <div>Total: ${total}</div>;
}

To test this, you must:

  • Import and render the component.
  • Mock the useState hook.
  • Trigger the useEffect.
  • Assert the DOM output.

This test is fragile: changing the component's internal state shape breaks the test, even if the business logic is unchanged.

The Benefits of Separation

Separate the calculation into a pure function:

function calculateCartTotal(items) {
return items.reduce((sum, item) => {
const discount = item.quantity > 5 ? 0.1 : 0;
return sum + item.price * item.quantity * (1 - discount);
}, 0);
}

Now you test it directly:

test('applies 10% discount for quantities > 5', () => {
const items = [{ price: 10, quantity: 6 }];
expect(calculateCartTotal(items)).toBe(54); // 10 * 6 * 0.9
});

The React component becomes a thin view:

function CartSummary({ items }) {
const total = calculateCartTotal(items);
return <div>Total: ${total}</div>;
}

This approach offers four concrete wins:

AspectTightly CoupledClean Architecture
TestingRender component, mock hooks, assert DOMImport function, call directly, assert value
ReuseLogic trapped in component treePure functions used anywhere: Vue, API, CLI
ChangeBusiness rule change may require component refactorBusiness rule change is isolated to one module
ReasoningMust understand React + business logicBusiness logic is plain JavaScript; React is the view

The Three Layers

Clean architecture for React typically has three layers:

  1. Domain layer: Pure functions, classes, and entities representing your business rules. No React imports. No HTTP calls. Examples: validation, calculations, state machines.
  2. Application layer: Use cases that orchestrate domain logic. Handles HTTP requests, database queries, and state updates. Agnostic to React.
  3. Presentation layer: React components that consume use cases and render views. Components call use case functions, handle React state, manage side effects.

A feature like "fetch and display a user profile" maps like this:

  • Domain: User entity, validateEmail() function.
  • Application: GetUserUseCase class with a execute(userId) method that calls the API and returns a User.
  • Presentation: UserProfile component that calls GetUserUseCase, stores the result in state, and renders it.

Each layer depends only on the layer below it. Components depend on use cases; use cases depend on domain. Never the reverse.

Key Takeaways

  • Separation of concerns means each module has one reason to change: business rules change the domain layer, UI changes the presentation layer.
  • Tight coupling forces you to re-test and re-reason about unrelated code when requirements shift.
  • Pure functions are faster to test, easier to reason about, and reusable across contexts.
  • Three-layer architecture (domain, application, presentation) is the most practical pattern for React apps.
  • Starting with separation prevents costly refactoring later.

Frequently Asked Questions

Does clean architecture add overhead for small projects?

For a 3-page hobby project, no. But for any team project or application with >500 lines of business logic, yes—and the overhead pays for itself in maintainability. If you anticipate the app will grow or be maintained by multiple people, separating concerns early prevents costly refactoring.

Can I use clean architecture with Next.js?

Yes. Next.js still renders React components; the same principles apply. Place domain and use case logic in standalone modules (not in pages/ or api/ routes), and call them from your components or API handlers. API routes act as adapters between your use cases and external clients.

What if my app doesn't have much business logic?

If your app is purely a content display (blog, portfolio), separation is lighter. But as soon as you validate input, calculate values, or manage complex state, the cost of tight coupling rises. Separate even small amounts of logic; the habit prepares you for growth.

Is this the same as Redux or state management?

No. Clean architecture is about code organization and dependency flow; state management is about where and how to store runtime data. You can use clean architecture with Redux, Zustand, or React Context. The principles are orthogonal.

Further Reading