← Назад к вопросам
Как реализовать удаление элемента списка по 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>
);
}
Лучшие практики
- Всегда используй id (не index) для key в списках
- Фильтруй или удаляй, но не мутируй состояние
- Обрабатывай ошибки и откатывай UI если нужно
- Показывай loader во время удаления
- Подтверждай удаление для критических операций
- Используй Context/Redux для глобального состояния
- Оптимистичные обновления улучшают UX если есть fallback
- Отключай кнопку во время операции чтобы избежать дублей