← Назад к вопросам

Как в гексагональной архитектуре разместить клиентское приложение на React?

1.7 Middle🔥 161 комментариев
#React#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

React в гексагональной архитектуре

Что такое гексагональная архитектура?

Гексагональная архитектура (Hexagonal Architecture или Ports & Adapters) — это паттерн, где бизнес-логика находится в центре ядра (domain), а всё остальное (UI, БД, внешние сервисы) — это adaptors на периметре.

┌────────────────────────────────────────────────┐
│  Ports (границы)                               │
│  ┌─────────────────────────────────────────┐  │
│  │                                         │  │
│  │  ┌─────────────────────────────────┐   │  │
│  │  │   Domain (бизнес-логика)       │   │  │
│  │  │   - Use Cases                   │   │  │
│  │  │   - Entities                    │   │  │
│  │  │   - Value Objects               │   │  │
│  │  └─────────────────────────────────┘   │  │
│  │                                         │  │
│  └─────────────────────────────────────────┘  │
│  ▲         ▲         ▲         ▲         ▲    │
│  │ Port 1  │ Port 2  │ Port 3  │ Port N  │    │
└──┼────────┼────────┼────────┼────────┼─┘
   │        │        │        │        │
┌──▼──────┬─▼──────┬─▼──────┬─▼──────┬─▼──────────┐
│ Adapter │Adapter │Adapter │Adapter │ Adapter    │
│  (UI)   │ (HTTP) │ (DB)   │ (Cache)│ (Queue)    │
└─────────┴────────┴────────┴────────┴────────────┘

Позиция React в этой архитектуре

React — это Adapter (UI Adapter), а не часть domain. React находится на периметре гексагона, отделённый от бизнес-логики портом.

Структура папок

project/
├── src/
│   │
│   ├── domain/                    # Ядро: бизнес-логика
│   │   ├── entities/              # Основные сущности
│   │   │   ├── User.ts
│   │   │   ├── Product.ts
│   │   │   └── Order.ts
│   │   ├── use-cases/             # Use cases (interactors)
│   │   │   ├── AuthenticateUser.ts
│   │   │   ├── CreateOrder.ts
│   │   │   └── GetUserProfile.ts
│   │   ├── repositories/          # Интерфейсы (ports)
│   │   │   ├── IUserRepository.ts
│   │   │   ├── IProductRepository.ts
│   │   │   └── IOrderRepository.ts
│   │   └── value-objects/         # Value objects
│   │       ├── Email.ts
│   │       ├── Price.ts
│   │       └── OrderStatus.ts
│   │
│   ├── application/               # Application layer
│   │   ├── dtos/                  # Data Transfer Objects
│   │   │   ├── CreateUserDTO.ts
│   │   │   ├── UserResponseDTO.ts
│   │   │   └── OrderDTO.ts
│   │   └── services/              # Бизнес-сервисы
│   │       └── UserService.ts
│   │
│   ├── infrastructure/            # Adaptors (реализация портов)
│   │   ├── persistence/           # БД adapter
│   │   │   ├── DatabaseUserRepository.ts
│   │   │   ├── DatabaseProductRepository.ts
│   │   │   └── index.ts
│   │   ├── http/                  # HTTP adapter (for backend)
│   │   │   ├── HttpUserRepository.ts
│   │   │   ├── client.ts          # Fetch/Axios instance
│   │   │   └── index.ts
│   │   └── cache/                 # Cache adapter
│   │       └── LocalStorageCache.ts
│   │
│   ├── presentation/              # React Adapter (UI)
│   │   ├── components/            # React компоненты
│   │   │   ├── UserProfile.tsx
│   │   │   ├── OrderForm.tsx
│   │   │   └── ProductList.tsx
│   │   ├── hooks/                 # Custom hooks (connect domain to React)
│   │   │   ├── useAuth.ts
│   │   │   ├── useOrders.ts
│   │   │   └── useProducts.ts
│   │   ├── pages/                 # Next.js pages или App pages
│   │   │   ├── /profile
│   │   │   ├── /orders
│   │   │   └── /checkout
│   │   ├── contexts/              # React Context (state management)
│   │   │   ├── AuthContext.tsx
│   │   │   └── OrderContext.tsx
│   │   └── lib/                   # Утилиты UI слоя
│   │       ├── api-config.ts
│   │       └── ui-utils.ts
│   │
│   └── main.tsx                   # Точка входа React
│
└── package.json

Пример: Контакт между слоями

1. Domain слой (бизнес-логика)

// domain/entities/User.ts
export class User {
  constructor(
    public id: string,
    public name: string,
    public email: string,
    public role: "admin" | "user"
  ) {}

  isAdmin(): boolean {
    return this.role === "admin";
  }

  updateProfile(name: string): User {
    return new User(this.id, name, this.email, this.role);
  }
}

// domain/use-cases/GetUserProfile.ts
export interface IUserRepository {
  getUserById(id: string): Promise<User | null>;
  updateUser(user: User): Promise<void>;
}

export class GetUserProfileUseCase {
  constructor(private userRepository: IUserRepository) {}

  async execute(userId: string): Promise<User> {
    const user = await this.userRepository.getUserById(userId);
    if (!user) {
      throw new Error("User not found");
    }
    return user;
  }
}

Note: domain слой НЕ знает о React, HTTP, БД. Он знает только об интерфейсах (ports).

2. Infrastructure слой (реализация портов)

// infrastructure/http/HttpUserRepository.ts
import { IUserRepository } from "@/domain/repositories/IUserRepository";
import { User } from "@/domain/entities/User";

export class HttpUserRepository implements IUserRepository {
  constructor(private baseUrl: string) {}

  async getUserById(id: string): Promise<User | null> {
    const response = await fetch(`${this.baseUrl}/users/${id}`);
    if (!response.ok) return null;

    const data = await response.json();
    return new User(data.id, data.name, data.email, data.role);
  }

  async updateUser(user: User): Promise<void> {
    await fetch(`${this.baseUrl}/users/${user.id}`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        name: user.name,
        email: user.email,
        role: user.role,
      }),
    });
  }
}

Eту реализацию можно swap на другую (DatabaseUserRepository, CachedUserRepository), не меняя domain.

3. Presentation слой (React)

// presentation/hooks/useAuth.ts
import { useState, useEffect } from "react";
import { User } from "@/domain/entities/User";
import { GetUserProfileUseCase } from "@/domain/use-cases/GetUserProfile";

export function useAuth(userRepository: IUserRepository) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const useCase = new GetUserProfileUseCase(userRepository);
    
    useCase
      .execute("current-user-id")
      .then(setUser)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }, [userRepository]);

  return { user, loading, error };
}

// presentation/components/UserProfile.tsx
export function UserProfile() {
  const { user, loading, error } = useAuth(userRepository);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>Email: {user?.email}</p>
      {user?.isAdmin() && <p>Admin privileges</p>}
    </div>
  );
}

React компонент не знает как загружаются данные (HTTP, БД, cache). Он просто использует hook, который использует use case.

Dependency Injection для связи слоев

// main.tsx (точка входа)
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./presentation/App";

// 1. Создаём infrastructure
const userRepository = new HttpUserRepository("https://api.example.com");
const productRepository = new HttpProductRepository("https://api.example.com");

// 2. Инжектируем в контекст или провайдер
const services = {
  userRepository,
  productRepository,
};

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App services={services} />
  </React.StrictMode>
);

// presentation/App.tsx
type AppProps = {
  services: typeof services;
};

export function App({ services }: AppProps) {
  return (
    <AuthProvider userRepository={services.userRepository}>
      <ProductProvider productRepository={services.productRepository}>
        <Routes />
      </ProductProvider>
    </AuthProvider>
  );
}

Почему так структурировать?

1. Независимость от framework

// Если захочу переойти с React на Vue/Svelte, domain всё-ещё работает
// Просто переписываю presentation слой

2. Легко тестировать

// Тестирую domain без React, без HTTP запросов
class MockUserRepository implements IUserRepository {
  async getUserById(id: string): Promise<User | null> {
    return new User("1", "Test User", "test@example.com", "user");
  }
}

const useCase = new GetUserProfileUseCase(new MockUserRepository());
const user = await useCase.execute("1");
assert.equal(user.name, "Test User");

3. Легко менять реализацию

// Была HTTP, хочу перейти на IndexedDB
const userRepository = new IndexedDBUserRepository();
// Всё работает, domain не изменился

4. Масштабируемость

// Легко добавить новый adapter (кеш, WebSocket, SSE)
class CachedUserRepository implements IUserRepository {
  constructor(
    private database: HttpUserRepository,
    private cache: LocalStorageCache
  ) {}

  async getUserById(id: string): Promise<User | null> {
    const cached = this.cache.get(`user-${id}`);
    if (cached) return cached;
    
    const user = await this.database.getUserById(id);
    if (user) this.cache.set(`user-${id}`, user);
    return user;
  }
}

Правила слоёв (Dependency Direction)

presentation → application → domain ← infrastructure
     ↓                         ↑
   React                    Ports
    UI                    (interfaces)

Важно: domain не зависит от presentation и infrastructure. Они зависят от domain.

Чек-лист применения

  • Вся бизнес-логика в domain/ (entities, use-cases, value-objects)
  • Интерфейсы (ports) в domain/repositories/
  • Реализация в infrastructure/
  • React компоненты в presentation/, не содержат бизнес-логики
  • Dependency injection в main.tsx
  • Domain легко тестировать без React
  • Можно swap infrastructure без изменения domain

Выводы: React в гексагональной архитектуре — это UI adapter, отделённый от бизнес-логики через ports. Это обеспечивает гибкость, тестируемость и масштабируемость.

Как в гексагональной архитектуре разместить клиентское приложение на React? | PrepBro