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

Как реализовать удаление элемента списка по id в React?

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

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

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

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

Как реализовать удаление элемента списка по id в React

Это типичная операция в React приложениях. Нужно правильно управлять состоянием, обновлять DOM эффективно и обрабатывать асинхронные операции. Рассмотрю несколько подходов от простого к сложному.

Простой подход: локальное состояние

import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, title: 'Learn React' },
    { id: 2, title: 'Build App' },
    { id: 3, title: 'Deploy' }
  ]);
  
  // Метод удаления: фильтруем todos, убирая нужный
  const handleDelete = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {todo.title}
          <button onClick={() => handleDelete(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

С API запросом

import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, title: 'Learn React' },
    { id: 2, title: 'Build App' }
  ]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const handleDelete = async (id) => {
    setLoading(true);
    setError(null);
    
    try {
      // Удаляем с бэкенда
      const response = await fetch(`/api/todos/${id}`, {
        method: 'DELETE'
      });
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      // Удаляем из состояния только если запрос успешен
      setTodos(todos.filter(todo => todo.id !== id));
    } catch (err) {
      setError(err.message);
      console.error('Delete failed:', err);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div>
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.title}
            <button 
              onClick={() => handleDelete(todo.id)}
              disabled={loading}
            >
              {loading ? 'Deleting...' : 'Delete'}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

С оптимистичным обновлением UI

// Удалить из UI сразу, а затем подтвердить с сервером
import { useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, title: 'Learn React' },
    { id: 2, title: 'Build App' }
  ]);
  const [error, setError] = useState(null);
  
  const handleDelete = async (id) => {
    // Сохраняем старое состояние на случай отката
    const oldTodos = todos;
    
    // Удаляем сразу из UI (оптимистично)
    setTodos(todos.filter(todo => todo.id !== id));
    
    try {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'DELETE'
      });
      
      if (!response.ok) {
        throw new Error('Delete failed');
      }
    } catch (err) {
      // Если ошибка, откатываем UI
      setTodos(oldTodos);
      setError('Failed to delete. Please try again.');
    }
  };
  
  return (
    <div>
      {error && (
        <div style={{ color: 'red', marginBottom: '10px' }}>
          {error}
          <button onClick={() => setError(null)}>Dismiss</button>
        </div>
      )}
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  );
}

С состоянием для каждого элемента

// Если нужна независимая индикация загрузки для каждого элемента
import { useState } from 'react';

function TodoItem({ todo, onDelete }) {
  const [isDeleting, setIsDeleting] = useState(false);
  
  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      await fetch(`/api/todos/${todo.id}`, { method: 'DELETE' });
      onDelete(todo.id);
    } catch (err) {
      console.error(err);
      setIsDeleting(false);
    }
  };
  
  return (
    <li>
      {todo.title}
      <button 
        onClick={handleDelete}
        disabled={isDeleting}
      >
        {isDeleting ? 'Deleting...' : 'Delete'}
      </button>
    </li>
  );
}

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, title: 'Learn React' },
    { id: 2, title: 'Build App' }
  ]);
  
  const handleDelete = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} onDelete={handleDelete} />
      ))}
    </ul>
  );
}

С подтверждением перед удалением

import { useState } from 'react';

function TodoItem({ todo, onDelete }) {
  const [isDeleting, setIsDeleting] = useState(false);
  const [showConfirm, setShowConfirm] = useState(false);
  
  const handleConfirmDelete = async () => {
    setIsDeleting(true);
    try {
      const response = await fetch(`/api/todos/${todo.id}`, {
        method: 'DELETE'
      });
      
      if (!response.ok) {
        throw new Error('Delete failed');
      }
      
      onDelete(todo.id);
      setShowConfirm(false);
    } catch (err) {
      alert('Failed to delete');
      setIsDeleting(false);
    }
  };
  
  if (showConfirm) {
    return (
      <li>
        {todo.title}
        <div>
          <p>Are you sure?</p>
          <button 
            onClick={handleConfirmDelete}
            disabled={isDeleting}
          >
            {isDeleting ? 'Deleting...' : 'Yes, Delete'}
          </button>
          <button 
            onClick={() => setShowConfirm(false)}
            disabled={isDeleting}
          >
            Cancel
          </button>
        </div>
      </li>
    );
  }
  
  return (
    <li>
      {todo.title}
      <button onClick={() => setShowConfirm(true)}>Delete</button>
    </li>
  );
}

С Context (для больших приложений)

import { createContext, useState, useCallback } from 'react';

// Создаем контекст
const TodoContext = createContext();

export function TodoProvider({ children }) {
  const [todos, setTodos] = useState([
    { id: 1, title: 'Learn React' },
    { id: 2, title: 'Build App' }
  ]);
  
  const deleteTodo = useCallback(async (id) => {
    try {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'DELETE'
      });
      
      if (!response.ok) throw new Error('Delete failed');
      
      // Оптимистичное обновление
      setTodos(prev => prev.filter(todo => todo.id !== id));
    } catch (err) {
      // Откатываем если ошибка
      console.error(err);
      // Можно сделать refetch или показать ошибку
      throw err;
    }
  }, []);
  
  return (
    <TodoContext.Provider value={{ todos, deleteTodo }}>
      {children}
    </TodoContext.Provider>
  );
}

// Используем контекст
import { useContext } from 'react';

function TodoItem({ todo }) {
  const { deleteTodo } = useContext(TodoContext);
  const [isDeleting, setIsDeleting] = useState(false);
  
  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      await deleteTodo(todo.id);
    } catch (err) {
      console.error('Delete failed');
      setIsDeleting(false);
    }
  };
  
  return (
    <li>
      {todo.title}
      <button onClick={handleDelete} disabled={isDeleting}>
        Delete
      </button>
    </li>
  );
}

С reducer (для сложного состояния)

import { useReducer } from 'react';

const todoReducer = (state, action) => {
  switch (action.type) {
    case 'SET_TODOS':
      return { ...state, todos: action.payload, loading: false };
    case 'START_DELETE':
      return { ...state, deleting: action.payload };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(t => t.id !== action.payload),
        deleting: null
      };
    case 'DELETE_ERROR':
      return { ...state, error: action.payload, deleting: null };
    default:
      return state;
  }
};

function TodoList() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [
      { id: 1, title: 'Learn React' },
      { id: 2, title: 'Build App' }
    ],
    deleting: null,
    error: null,
    loading: false
  });
  
  const handleDelete = async (id) => {
    dispatch({ type: 'START_DELETE', payload: id });
    
    try {
      const response = await fetch(`/api/todos/${id}`, {
        method: 'DELETE'
      });
      
      if (!response.ok) {
        throw new Error('Delete failed');
      }
      
      dispatch({ type: 'DELETE_TODO', payload: id });
    } catch (err) {
      dispatch({ type: 'DELETE_ERROR', payload: err.message });
    }
  };
  
  return (
    <div>
      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}
      <ul>
        {state.todos.map(todo => (
          <li key={todo.id}>
            {todo.title}
            <button 
              onClick={() => handleDelete(todo.id)}
              disabled={state.deleting === todo.id}
            >
              {state.deleting === todo.id ? 'Deleting...' : 'Delete'}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

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

  1. Всегда используй id (не index) для key в списках
  2. Фильтруй или удаляй, но не мутируй состояние
  3. Обрабатывай ошибки и откатывай UI если нужно
  4. Показывай loader во время удаления
  5. Подтверждай удаление для критических операций
  6. Используй Context/Redux для глобального состояния
  7. Оптимистичные обновления улучшают UX если есть fallback
  8. Отключай кнопку во время операции чтобы избежать дублей
Как реализовать удаление элемента списка по id в React? | PrepBro