Сейчас приложение умеет только показывать задачи. Добавим три действия: создать, удалить, переключить выполнение. Используем три разных HTTP-метода: POST, DELETE, PATCH — и применим важный приём: оптимистичный UI.
Что такое оптимистичный UI
Когда пользователь нажал «добавить» или «удалить», возможны два подхода.
Пессимистичный:
- Заблокировать форму.
- Дождаться ответа сервера.
- Если успех — обновить состояние и перерисовать.
- Разблокировать.
Безопасно, но медленно. Пользователь смотрит на спиннер, пока летит запрос.
Оптимистичный:
- Сразу обновить локальное состояние и перерисовать.
- Параллельно отправить запрос.
- Если сервер ответил ошибкой — откатить изменение и показать сообщение.
Кажется мгновенным. В 99% случаев сервер согласен — пользователь не видит задержки. На 1% ошибок откат корректно возвращает в прежнее состояние.
В нашем проекте используем оптимистичный подход — он практичнее и научит работать с реальным паттерном.
Создание задачи
const formEl = document.querySelector('#form');
const inputEl = document.querySelector('#input');
formEl.addEventListener('submit', async (e) => {
e.preventDefault();
const title = inputEl.value.trim();
if (!title) return;
// Оптимистично добавляем задачу с временным id:
const tempId = `tmp-${Date.now()}`;
const optimistic = { id: tempId, title, completed: false, userId: 1 };
todos.push(optimistic);
inputEl.value = '';
render();
try {
const saved = await api('/todos', {
method: 'POST',
body: JSON.stringify({ title, completed: false, userId: 1 }),
});
// Сервер вернул запись с настоящим id — подменяем временную:
const idx = todos.findIndex(t => t.id === tempId);
if (idx !== -1) todos[idx] = saved;
render();
} catch (error) {
// Откатываем — убираем временную задачу:
todos = todos.filter(t => t.id !== tempId);
render();
statusEl.textContent = 'Не получилось добавить: ' + error.message;
}
});
Хитрость с временным id — чтобы было что подменить, когда придёт настоящий id от сервера. Префикс tmp- отличает временные id от настоящих числовых.
Удаление
async function deleteTodo(id) {
// Сохраняем удаляемую задачу — на случай отката:
const removed = todos.find(t => t.id === id);
todos = todos.filter(t => t.id !== id);
render();
try {
await api(`/todos/${id}`, { method: 'DELETE' });
} catch (error) {
// Откат — возвращаем задачу:
if (removed) todos.push(removed);
render();
statusEl.textContent = 'Не удалось удалить: ' + error.message;
}
}
Запрос идёт по конкретному URL ресурса: /todos/42. Тела нет, потому что DELETE и не должно ничего нести (см. главу 3.1).
Если задача добавлялась оптимистично и ещё не успела получить настоящий id — в id будет tmp-.... Делать DELETE для несуществующего ресурса бессмысленно — добавим проверку:
async function deleteTodo(id) {
const removed = todos.find(t => t.id === id);
todos = todos.filter(t => t.id !== id);
render();
// Не дёргаем сервер для ещё не сохранённой задачи:
if (typeof id === 'string' && id.startsWith('tmp-')) return;
try {
await api(`/todos/${id}`, { method: 'DELETE' });
} catch (error) {
if (removed) todos.push(removed);
render();
statusEl.textContent = 'Не удалось удалить: ' + error.message;
}
}
Переключение статуса
async function toggleTodo(id, completed) {
// Оптимистично обновляем локально:
const todo = todos.find(t => t.id === id);
if (!todo) return;
const previous = todo.completed;
todo.completed = completed;
render();
if (typeof id === 'string' && id.startsWith('tmp-')) return;
try {
await api(`/todos/${id}`, {
method: 'PATCH',
body: JSON.stringify({ completed }),
});
} catch (error) {
// Откат:
todo.completed = previous;
render();
statusEl.textContent = 'Не удалось переключить: ' + error.message;
}
}
Тут используем PATCH — передаём только одно изменившееся поле, ровно как в главе 5.4.
Что в итоге
На этом моменте у нас рабочий CRUD-клиент. Все четыре операции (Create, Read, Update, Delete) задействованы. Код — меньше ста строк, с оптимистичным UI и нормальными ошибочными ветками.
Дальше остался косметический штрих — индикаторы загрузки, нормальные сообщения об ошибках, блокировки кнопок во время операций. Об этом — в последней главе модуля.