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

Как выглядит CRUD для списка пользователей?

2.0 Middle🔥 141 комментариев
#React

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

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

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

CRUD для списка пользователей на фронтенде

Этот вопрос проверяет практическое понимание работы с API и управлением состоянием в React приложении. CRUD = Create, Read, Update, Delete.

API Endpoints для пользователей

GET    /api/v1/users          - получить всех пользователей
GET    /api/v1/users/{id}     - получить одного пользователя
POST   /api/v1/users          - создать пользователя
PUT    /api/v1/users/{id}     - обновить пользователя
DELETE /api/v1/users/{id}     - удалить пользователя

Полный пример на React + TypeScript

// types/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
  createdAt: string;
}

export interface CreateUserData {
  name: string;
  email: string;
  role: 'admin' | 'user';
}

Хук для работы с API

// hooks/useUsers.ts
import { useState, useCallback } from 'react';
import { User, CreateUserData } from '@/types/user';

export function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // READ - получить всех пользователей
  const fetchUsers = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await fetch('/api/v1/users');
      if (!response.ok) throw new Error('Failed to fetch users');
      const data = await response.json();
      setUsers(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setLoading(false);
    }
  }, []);

  // CREATE - создать пользователя
  const createUser = useCallback(async (userData: CreateUserData) => {
    setError(null);
    try {
      const response = await fetch('/api/v1/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      });
      if (!response.ok) throw new Error('Failed to create user');
      const newUser = await response.json();
      setUsers(prev => [...prev, newUser]);
      return newUser;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
      throw err;
    }
  }, []);

  // UPDATE - обновить пользователя
  const updateUser = useCallback(async (id: string, userData: Partial<CreateUserData>) => {
    setError(null);
    try {
      const response = await fetch(`/api/v1/users/${id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      });
      if (!response.ok) throw new Error('Failed to update user');
      const updatedUser = await response.json();
      setUsers(prev => prev.map(u => u.id === id ? updatedUser : u));
      return updatedUser;
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
      throw err;
    }
  }, []);

  // DELETE - удалить пользователя
  const deleteUser = useCallback(async (id: string) => {
    setError(null);
    try {
      const response = await fetch(`/api/v1/users/${id}`, {
        method: 'DELETE'
      });
      if (!response.ok) throw new Error('Failed to delete user');
      setUsers(prev => prev.filter(u => u.id !== id));
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
      throw err;
    }
  }, []);

  return {
    users,
    loading,
    error,
    fetchUsers,
    createUser,
    updateUser,
    deleteUser
  };
}

Компонент со списком пользователей

// components/users/UsersList.tsx
import { useEffect, useState } from 'react';
import { useUsers } from '@/hooks/useUsers';
import { CreateUserData, User } from '@/types/user';

export function UsersList() {
  const { users, loading, error, fetchUsers, createUser, updateUser, deleteUser } = useUsers();
  const [editingId, setEditingId] = useState<string | null>(null);
  const [showForm, setShowForm] = useState(false);
  const [formData, setFormData] = useState<CreateUserData>({
    name: '',
    email: '',
    role: 'user'
  });

  // READ - загрузить при монтировании
  useEffect(() => {
    fetchUsers();
  }, [fetchUsers]);

  // CREATE - добавить нового пользователя
  const handleCreate = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await createUser(formData);
      setFormData({ name: '', email: '', role: 'user' });
      setShowForm(false);
    } catch (err) {
      console.error('Failed to create user', err);
    }
  };

  // UPDATE - начать редактирование
  const handleEditStart = (user: User) => {
    setEditingId(user.id);
    setFormData({
      name: user.name,
      email: user.email,
      role: user.role
    });
  };

  const handleEditSave = async (id: string) => {
    try {
      await updateUser(id, formData);
      setEditingId(null);
      setFormData({ name: '', email: '', role: 'user' });
    } catch (err) {
      console.error('Failed to update user', err);
    }
  };

  // DELETE - удалить пользователя
  const handleDelete = async (id: string) => {
    if (!window.confirm('Вы уверены?')) return;
    try {
      await deleteUser(id);
    } catch (err) {
      console.error('Failed to delete user', err);
    }
  };

  return (
    <div className="space-y-4">
      <h1 className="text-2xl font-bold">Пользователи</h1>

      {error && <div className="bg-red-100 p-4 rounded">{error}</div>}

      {!showForm ? (
        <button
          onClick={() => setShowForm(true)}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Добавить пользователя
        </button>
      ) : (
        <form onSubmit={handleCreate} className="space-y-3 bg-gray-100 p-4 rounded">
          <input
            type="text"
            placeholder="Имя"
            value={formData.name}
            onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
            required
            className="w-full p-2 border rounded"
          />
          <input
            type="email"
            placeholder="Email"
            value={formData.email}
            onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
            required
            className="w-full p-2 border rounded"
          />
          <select
            value={formData.role}
            onChange={(e) => setFormData(prev => ({ ...prev, role: e.target.value as 'admin' | 'user' }))}
            className="w-full p-2 border rounded"
          >
            <option value="user">User</option>
            <option value="admin">Admin</option>
          </select>
          <div className="flex gap-2">
            <button type="submit" className="px-4 py-2 bg-green-500 text-white rounded">
              Создать
            </button>
            <button
              type="button"
              onClick={() => setShowForm(false)}
              className="px-4 py-2 bg-gray-400 text-white rounded"
            >
              Отмена
            </button>
          </div>
        </form>
      )}

      {loading ? (
        <div>Загрузка...</div>
      ) : (
        <table className="w-full border-collapse border border-gray-300">
          <thead>
            <tr className="bg-gray-200">
              <th className="border p-2">Имя</th>
              <th className="border p-2">Email</th>
              <th className="border p-2">Роль</th>
              <th className="border p-2">Действия</th>
            </tr>
          </thead>
          <tbody>
            {users.map(user => (
              <tr key={user.id} className="border">
                <td className="border p-2">
                  {editingId === user.id ? (
                    <input
                      type="text"
                      value={formData.name}
                      onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
                      className="w-full p-1 border rounded"
                    />
                  ) : (
                    user.name
                  )}
                </td>
                <td className="border p-2">
                  {editingId === user.id ? (
                    <input
                      type="email"
                      value={formData.email}
                      onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
                      className="w-full p-1 border rounded"
                    />
                  ) : (
                    user.email
                  )}
                </td>
                <td className="border p-2">{user.role}</td>
                <td className="border p-2 flex gap-2">
                  {editingId === user.id ? (
                    <>
                      <button
                        onClick={() => handleEditSave(user.id)}
                        className="px-2 py-1 bg-green-500 text-white rounded text-sm"
                      >
                        Сохранить
                      </button>
                      <button
                        onClick={() => setEditingId(null)}
                        className="px-2 py-1 bg-gray-400 text-white rounded text-sm"
                      >
                        Отмена
                      </button>
                    </>
                  ) : (
                    <>
                      <button
                        onClick={() => handleEditStart(user)}
                        className="px-2 py-1 bg-yellow-500 text-white rounded text-sm"
                      >
                        Изменить
                      </button>
                      <button
                        onClick={() => handleDelete(user.id)}
                        className="px-2 py-1 bg-red-500 text-white rounded text-sm"
                      >
                        Удалить
                      </button>
                    </>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
    </div>
  );
}

Использование в приложении

// app/admin/users/page.tsx
import { UsersList } from '@/components/users/UsersList';

export default function UsersPage() {
  return (
    <div className="container mx-auto py-8">
      <UsersList />
    </div>
  );
}

Лучшие практики

  1. Оптимистичные обновления - обновить UI перед получением ответа сервера
  2. Обработка ошибок - показать пользователю понятное сообщение
  3. Loading статы - дать фидбэк о загрузке
  4. Валидация - проверить данные перед отправкой
  5. Типизация - использовать TypeScript для безопасности

На собеседовании

Хороший ответ показывает:

  • Понимание REST API методов
  • Использование fetch API
  • Управление состоянием в React
  • Обработку ошибок и loading статов
  • Практическую реализацию CRUD операций

Это демонстрирует готовность к реальной фронтенд-разработке.