← Назад к вопросам
Как сделаешь композицию компонента с логикой рендера и запросов с 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) где нужно
- Тестируй компоненты отдельно (логика + представление)
Правильная композиция компонентов позволяет писать чистый, переиспользуемый и тестируемый код.