Пошук уроків, статей та іншого контенту
Реалізуєте створення, оновлення та видалення ресурсів через POST, PUT, PATCH і DELETE.
У REST API різні операції над ресурсами виконують за допомогою HTTP-методів:
POST — створення нового ресурсу;
PUT — повна заміна ресурсу;
PATCH — часткове оновлення ресурсу;
DELETE — видалення ресурсу.
У React ці методи зазвичай викликають через fetch або бібліотеку для HTTP-запитів. У цьому уроці використаємо вбудований fetch.
Приклад ресурсу post:
{
"id": 1,
"title": "Заголовок",
"body": "Текст публікації"
}Для кожного методу важливо правильно сформувати:
URL ресурсу;
HTTP-метод;
заголовки;
тіло запиту, якщо воно потрібне;
обробку відповіді та помилок.
Метод POST використовується, коли потрібно створити новий ресурс у колекції.
POST /postsТіло запиту може містити дані нового ресурсу:
{
"title": "Нова публікація",
"body": "Текст нової публікації"
}У fetch JSON потрібно явно перетворити на рядок за допомогою JSON.stringify:
const response = await fetch("/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Нова публікація",
body: "Текст нової публікації",
}),
});
if (!response.ok) {
throw new Error("Не вдалося створити публікацію");
}
const createdPost = await response.json();Сервер зазвичай повертає створений ресурс разом із його ідентифікатором:
{
"id": 42,
"title": "Нова публікація",
"body": "Текст нової публікації"
}У React після успішного POST можна додати отриманий ресурс до локального стану:
setPosts((currentPosts) => [createdPost, ...currentPosts]);Метод PUT застосовують для повної заміни вже наявного ресурсу:
PUT /posts/42Запит зазвичай містить усі поля ресурсу:
{
"title": "Оновлений заголовок",
"body": "Оновлений текст"
}Приклад запиту:
const response = await fetch(`/posts/${postId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Оновлений заголовок",
body: "Оновлений текст",
}),
});Якщо ресурс має додаткові обов’язкові поля, їх також потрібно передати. Наприклад, передавання лише title через PUT може призвести до втрати body, якщо сервер трактує запит як повну заміну.
Метод PATCH призначений для зміни лише окремих полів:
PATCH /posts/42Наприклад, можна оновити тільки заголовок:
const response = await fetch(`/posts/${postId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Новий заголовок",
}),
});Інші поля ресурсу при цьому мають залишитися без змін.
Вибір між PUT і PATCH залежить від контракту API:
використовуйте PUT, коли надсилаєте повний стан ресурсу;
використовуйте PATCH, коли змінюєте лише частину ресурсу.
Метод DELETE видаляє ресурс за ідентифікатором:
DELETE /posts/42Для простого видалення тіло запиту зазвичай не потрібне:
const response = await fetch(`/posts/${postId}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Не вдалося видалити публікацію");
}Після успішного видалення потрібно прибрати ресурс із локального стану:
setPosts((currentPosts) =>
currentPosts.filter((post) => post.id !== postId)
);Сервер може повернути статус 204 No Content. У такому випадку не можна безумовно викликати response.json(), оскільки відповідь не містить JSON.
fetch не вважає HTTP-помилки винятками автоматично. Наприклад, відповідь зі статусом 404 або 500 не спричинить catch сама по собі.
Тому потрібно перевіряти response.ok:
async function requestJson(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP-помилка: ${response.status}`);
}
if (response.status === 204) {
return null;
}
return response.json();
}Тепер цю функцію можна використовувати для різних методів:
const createdPost = await requestJson("/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "Заголовок",
body: "Текст",
}),
});Окремо варто обробляти:
стан завантаження;
повідомлення про помилку;
успішну відповідь;
оновлення локального стану.
Нижче наведено компонент, який демонструє всі чотири методи. Для прикладу використано тестовий API. Він приймає запити та повертає відповіді, але зміни не зберігаються після завершення запиту.
import { useEffect, useState } from "react";
const API_URL = "https://jsonplaceholder.typicode.com/posts";
async function parseResponse(response) {
if (!response.ok) {
throw new Error(`Помилка сервера: ${response.status}`);
}
if (response.status === 204) {
return null;
}
return response.json();
}
export default function PostsManager() {
const [posts, setPosts] = useState([]);
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [editingId, setEditingId] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
async function loadPosts() {
try {
setLoading(true);
setError("");
const response = await fetch(`${API_URL}?_limit=5`);
const data = await parseResponse(response);
setPosts(data);
} catch (requestError) {
setError(requestError.message);
} finally {
setLoading(false);
}
}
loadPosts();
}, []);
async function handleSubmit(event) {
event.preventDefault();
if (!title.trim() || !body.trim()) {
setError("Заповніть заголовок і текст");
return;
}
try {
setLoading(true);
setError("");
if (editingId === null) {
// Створення нового ресурсу через POST
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
body,
userId: 1,
}),
});
const createdPost = await parseResponse(response);
setPosts((currentPosts) => [createdPost, ...currentPosts]);
} else {
// Часткове оновлення ресурсу через PATCH
const response = await fetch(`${API_URL}/${editingId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title,
body,
}),
});
const updatedPost = await parseResponse(response);
setPosts((currentPosts) =>
currentPosts.map((post) =>
post.id === editingId ? { ...post, ...updatedPost } : post
)
);
}
setTitle("");
setBody("");
setEditingId(null);
} catch (requestError) {
setError(requestError.message);
} finally {
setLoading(false);
}
}
function startEditing(post) {
setEditingId(post.id);
setTitle(post.title);
setBody(post.body);
setError("");
}
function cancelEditing() {
setEditingId(null);
setTitle("");
setBody("");
}
async function handleDelete(postId) {
try {
setLoading(true);
setError("");
// Видалення ресурсу через DELETE
const response = await fetch(`${API_URL}/${postId}`, {
method: "DELETE",
});
await parseResponse(response);
setPosts((currentPosts) =>
currentPosts.filter((post) => post.id !== postId)
);
if (editingId === postId) {
cancelEditing();
}
} catch (requestError) {
setError(requestError.message);
} finally {
setLoading(false);
}
}
return (
<main>
<h1>Публікації</h1>
<form onSubmit={handleSubmit}>
<h2>{editingId === null ? "Нова публікація" : "Редагування"}</h2>
<label>
Заголовок
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Введіть заголовок"
/>
</label>
<label>
Текст
<textarea
value={body}
onChange={(event) => setBody(event.target.value)}
placeholder="Введіть текст"
/>
</label>
<button type="submit" disabled={loading}>
{editingId === null ? "Створити" : "Зберегти"}
</button>
{editingId !== null && (
<button type="button" onClick={cancelEditing}>
Скасувати
</button>
)}
</form>
{loading && <p>Виконується запит...</p>}
{error && <p role="alert">{error}</p>}
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
<button type="button" onClick={() => startEditing(post)}>
Редагувати
</button>
<button type="button" onClick={() => handleDelete(post.id)}>
Видалити
</button>
</li>
))}
</ul>
</main>
);
}У компоненті реалізовано такий потік:
GET завантажує початковий список публікацій.
POST створює публікацію, коли editingId дорівнює null.
PATCH оновлює заголовок і текст під час редагування.
DELETE видаляє публікацію за її ідентифікатором.
Після кожного успішного запиту оновлюється стан posts.
У попередньому компоненті редагування виконується через PATCH. Якщо API вимагає повної заміни, запит можна змінити на PUT:
const response = await fetch(`${API_URL}/${editingId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
id: editingId,
title,
body,
userId: 1,
}),
});
const replacedPost = await parseResponse(response);
setPosts((currentPosts) =>
currentPosts.map((post) =>
post.id === editingId ? replacedPost : post
)
);У цьому випадку ми передаємо весь ресурс, а не тільки поля, які змінилися.
Після POST, PUT, PATCH або DELETE інтерфейс потрібно синхронізувати з відповіддю сервера.
Типові операції зі станом:
// Додати створений ресурс
setPosts((posts) => [createdPost, ...posts]);
// Замінити оновлений ресурс
setPosts((posts) =>
posts.map((post) => (post.id === updatedPost.id ? updatedPost : post))
);
// Видалити ресурс
setPosts((posts) =>
posts.filter((post) => post.id !== deletedPostId)
);Не варто змінювати масив стану напряму:
// Неправильно
posts.push(createdPost);
posts = posts.filter((post) => post.id !== deletedPostId);React очікує нове значення стану, тому використовуйте функціональний синтаксис setState і створюйте новий масив.
Content-TypeЯкщо тіло запиту містить JSON, передавайте відповідний заголовок:
headers: {
"Content-Type": "application/json",
}Без нього сервер може не розпізнати формат даних.
JSON.stringifyНеправильно:
body: {
title: "Заголовок",
}Правильно:
body: JSON.stringify({
title: "Заголовок",
})catchfetch не кидає помилку для статусів 400 або 500. Завжди перевіряйте response.ok.
response.json() для відповіді 204Відповідь 204 No Content не має тіла. Виклик response.json() у такому випадку може спричинити помилку.
Якщо передати через PUT лише одне поле, сервер може замінити ним увесь ресурс. Для часткового оновлення використовуйте PATCH, якщо це передбачено API.
Не видаляйте або не змінюйте ресурс у локальному стані до підтвердження сервера, якщо не реалізуєте окремий механізм оптимістичного оновлення. Інакше інтерфейс може показати успішну операцію, яка насправді завершилася помилкою.
POST створює новий ресурс у колекції.
PUT повністю замінює наявний ресурс.
PATCH змінює окремі поля ресурсу.
DELETE видаляє ресурс.
JSON у тілі запиту потрібно серіалізувати через JSON.stringify.
Для JSON-запитів потрібно вказувати Content-Type: application/json.
fetch потрібно доповнювати перевіркою response.ok.
Після успішної мутації локальний стан React потрібно синхронізувати з відповіддю API.