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

Как сделаешь композицию компонента с логикой рендера и запросов с Backend?

1.7 Middle🔥 181 комментариев
#JavaScript Core

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

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

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

Композиция компонентов с бизнес-логикой

1. Разделение ответственности: Container vs Presentational

Разделяю логику получения данных (Container) и отображение (Presentational):

// ===== Presentational Component (чистый рендер) =====
interface UserListProps {
  users: User[];
  isLoading: boolean;
  error: string | null;
  onUserClick: (user: User) => void;
}

export function UserListView({ users, isLoading, error, onUserClick }: UserListProps) {
  if (isLoading) return <div>Загрузка...</div>;
  if (error) return <div className="text-red-600">{error}</div>;
  
  return (
    <ul className="space-y-2">
      {users.map(user => (
        <li 
          key={user.id}
          onClick={() => onUserClick(user)}
          className="cursor-pointer p-2 hover:bg-gray-100"
        >
          {user.name} ({user.email})
        </li>
      ))}
    </ul>
  );
}

// ===== Container Component (логика + запросы) =====
export function UserListContainer() {
  const [users, setUsers] = useState<User[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetchUsers()
      .then(setUsers)
      .catch(err => setError(err.message))
      .finally(() => setIsLoading(false));
  }, []);

  const handleUserClick = (user: User) => {
    console.log('User clicked:', user);
  };

  return (
    <UserListView
      users={users}
      isLoading={isLoading}
      error={error}
      onUserClick={handleUserClick}
    />
  );
}

2. Кастомные хуки для логики

Выношу логику работы с Backend в переиспользуемый хук:

interface UseFetchResult<T> {
  data: T | null;
  isLoading: boolean;
  error: Error | null;
  refetch: () => Promise<void>;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const refetch = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      
      const json = await response.json();
      setData(json);
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Unknown error'));
    } finally {
      setIsLoading(false);
    }
  }, [url]);

  useEffect(() => {
    refetch();
  }, [refetch]);

  return { data, isLoading, error, refetch };
}

// Использование:
export function UserList() {
  const { data: users = [], isLoading, error, refetch } = useFetch<User[]>('/api/users');

  return (
    <div>
      <UserListView users={users} isLoading={isLoading} error={error?.message ?? null} />
      <button onClick={refetch} disabled={isLoading}>
        Обновить
      </button>
    </div>
  );
}

3. Слой сервисов для API

Создаю отдельный слой для работы с Backend:

// services/userService.ts
export const userService = {
  async getUsers(filters?: UserFilters): Promise<User[]> {
    const params = new URLSearchParams(filters as Record<string, string>);
    const response = await fetch(`/api/users?${params}`);
    
    if (!response.ok) {
      throw new Error(`Failed to fetch users: ${response.status}`);
    }
    
    return response.json();
  },

  async getUserById(id: string): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) throw new Error('User not found');
    return response.json();
  },

  async updateUser(id: string, data: Partial<User>): Promise<User> {
    const response = await fetch(`/api/users/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) throw new Error('Failed to update user');
    return response.json();
  },

  async deleteUser(id: string): Promise<void> {
    const response = await fetch(`/api/users/${id}`, {
      method: 'DELETE'
    });
    
    if (!response.ok) throw new Error('Failed to delete user');
  }
};

// Использование в компоненте
export function UserListContainer() {
  const [users, setUsers] = useState<User[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    userService.getUsers()
      .then(setUsers)
      .catch(err => setError(err.message))
      .finally(() => setIsLoading(false));
  }, []);

  return <UserListView users={users} isLoading={isLoading} error={error} />;
}

4. Хук + сервис комбинация

Объединяю хук и сервис в одно целое:

function useUsers(filters?: UserFilters) {
  const [users, setUsers] = useState<User[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const load = useCallback(async () => {
    setIsLoading(true);
    setError(null);
    
    try {
      const data = await userService.getUsers(filters);
      setUsers(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setIsLoading(false);
    }
  }, [filters]);

  useEffect(() => {
    load();
  }, [load]);

  return { users, isLoading, error, refetch: load };
}

// Использование:
export function UserListPage() {
  const { users, isLoading, error } = useUsers();

  return (
    <UserListView users={users} isLoading={isLoading} error={error} />
  );
}

5. Структура папок для масштабируемости

src/
├── components/
│   ├── UserList/
│   │   ├── UserList.tsx
│   │   ├── UserListContainer.tsx
│   │   ├── UserList.test.tsx
│   │   └── useUsers.ts
│   └── ...
├── services/
│   ├── userService.ts
│   ├── apiClient.ts
│   └── ...
├── hooks/
│   ├── useFetch.ts
│   ├── useAsync.ts
│   └── ...
└── types/
    ├── user.ts
    └── api.ts

6. Best Practices композиции

  • Разделяй логику (Container) и представление (Presentational)
  • Используй кастомные хуки для переиспользуемой логики
  • Создай слой сервисов для работы с API
  • Типизируй все props и возвращаемые значения
  • Обрабатывай ошибки на каждом уровне
  • Используй мемоизацию (useMemo, useCallback) где нужно
  • Тестируй компоненты отдельно (логика + представление)

Правильная композиция компонентов позволяет писать чистый, переиспользуемый и тестируемый код.

Как сделаешь композицию компонента с логикой рендера и запросов с Backend? | PrepBro