Пошук уроків, статей та іншого контенту
Розберете відмінності між серверним і клієнтським станом та визначите відповідальність кожного з них.
Стан — це дані, від яких залежить поведінка або відображення інтерфейсу. У React стан змінюється, а компонент повторно рендериться.
Наприклад:
список завдань користувача;
індикатор завантаження;
відкритість модального вікна;
значення текстового поля;
вибраний фільтр;
повідомлення про помилку.
Однак усі ці дані мають різне походження та різний життєвий цикл. За цією ознакою їх зручно поділити на server state і client state.
Server state, або стан сервера, — це дані, які належать серверу та надходять до клієнтського застосунку через API.
Прикладами є:
профіль користувача;
список товарів;
замовлення;
повідомлення;
права доступу;
результати пошуку;
статус завдання, збережений у базі даних.
Server state відрізняється від звичайних локальних змінних такими властивостями:
джерело істини розташоване на сервері;
дані можуть змінюватися незалежно від поточного відкритого компонента;
ті самі дані можуть використовувати кілька компонентів;
дані потрібно завантажувати асинхронно;
можливі стани loading, error і success;
дані можуть застаріти;
після мутації їх іноді потрібно повторно отримати із сервера;
дані можна кешувати.
React не знає, як отримувати або кешувати дані з сервера. Він лише відображає значення, які отримав компонент.
Для даних із сервера зазвичай потрібно врахувати такий цикл:
Компонент починає завантаження.
Відображається індикатор завантаження.
Сервер повертає дані або помилку.
Компонент показує результат.
Дані можуть бути оновлені, повторно завантажені або замінені після мутації.
Наприклад, список завдань не створюється самим React. React запитує його в API та відображає отриману відповідь.
Client state, або стан клієнта, — це дані, якими безпосередньо керує інтерфейс у браузері.
Прикладами є:
чи відкрите модальне вікно;
який елемент вибрано;
поточне значення поля форми;
активна вкладка;
увімкнений або вимкнений перемикач;
стан розгортання меню;
локальний текст помилки валідації;
поточний крок майстра.
Client state зазвичай:
належить конкретному компоненту або частині клієнтського застосунку;
змінюється у відповідь на дії користувача;
не потребує запиту до сервера;
існує лише під час роботи застосунку або поточного сеансу;
не є джерелом істини для даних у базі даних.
Наприклад, стан isModalOpen описує лише те, чи потрібно показати модальне вікно. Серверу не обов’язково знати про це значення.
Головна відмінність полягає не в тому, де зберігається значення в пам’яті, а в тому, хто володіє даними та відповідає за їхню актуальність.
| Server state | Client state | |---|---| | Дані належать серверу | Дані належать інтерфейсу | | Отримуються через API | Змінюються в коді клієнта | | Можуть бути спільними для багатьох компонентів і користувачів | Часто стосуються конкретного компонента | | Можуть застарівати | Зазвичай актуальні в межах поточного UI | | Потребують обробки завантаження та помилок | Зазвичай змінюються синхронно | | Можуть кешуватися та повторно запитуватися | Зазвичай зберігаються через useState, useReducer або Context |
Наприклад:
tasks із API — server state;
isTaskFormOpen — client state;
selectedTaskId — client state;
зміни завдання після успішного збереження — знову server state.
Компонент може одночасно працювати з обома типами стану. Наприклад, він завантажує завдання із сервера та зберігає локально вибране завдання.
import { useEffect, useState } from "react";
export default function TaskList() {
const [tasks, setTasks] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Стан клієнта: це рішення інтерфейсу, а не дані сервера
const [selectedTaskId, setSelectedTaskId] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function loadTasks() {
try {
setIsLoading(true);
setError(null);
const response = await fetch(
"https://jsonplaceholder.typicode.com/todos?_limit=5",
{ signal: controller.signal }
);
if (!response.ok) {
throw new Error("Не вдалося завантажити завдання");
}
const data = await response.json();
setTasks(data);
} catch (error) {
if (error.name !== "AbortError") {
setError(error.message);
}
} finally {
setIsLoading(false);
}
}
loadTasks();
return () => {
controller.abort();
};
}, []);
if (isLoading) {
return <p>Завантаження...</p>;
}
if (error) {
return <p role="alert">Помилка: {error}</p>;
}
return (
<section>
<h2>Завдання</h2>
<ul>
{tasks.map((task) => (
<li key={task.id}>
<button
type="button"
onClick={() => setSelectedTaskId(task.id)}
aria-pressed={selectedTaskId === task.id}
>
{task.title}
</button>
{selectedTaskId === task.id && <strong> — вибрано</strong>}
</li>
))}
</ul>
</section>
);
}У цьому прикладі:
tasks містить дані, отримані із сервера, тому це server state;
isLoading і error описують процес завантаження на клієнті, але пов’язані із server state;
selectedTaskId — client state, тому що лише інтерфейс вирішує, яке завдання вибрано;
useEffect запускає запит після монтування компонента;
AbortController скасовує запит, якщо компонент буде видалено до завершення запиту.
Важливо: той факт, що tasks зберігається у useState, не робить його client state. Визначальним є джерело та власник даних. Дані сервера можна тимчасово зберігати в локальному стані React, але вони все одно залишаються server state за своєю природою.
Поставте кілька запитань.
Якщо остаточне значення зберігається в базі даних або визначається API, це server state.
Поточне ім’я користувача → сервер
Чи відкритий список профілю → клієнтЯкщо зміна повинна бути доступною після повторного входу, на іншому пристрої або для іншого користувача, її потрібно зберегти на сервері.
Змінити адресу доставки → сервер
Відкрити меню користувача → клієнтЯкщо значення потрібно отримати через API, воно належить до server state.
Список замовлень → server state
Активна вкладка "Замовлення" → client stateТимчасові значення, які потрібні для роботи UI, зазвичай є client state.
Введений, але ще не відправлений текст форми → client state
Збережений коментар → server stateПоля форми добре показують різницю між двома типами стану.
Поки користувач вводить текст, значення є client state:
const [draft, setDraft] = useState("");Після успішної відправки сервер може зберегти цей текст. Тоді збережений коментар стає server state.
Отже, в одному процесі можуть брати участь обидва типи:
Користувач вводить значення — client state.
Клієнт відправляє його на сервер.
Сервер зберігає значення.
Збережений результат — server state.
Форма може очистити свій локальний client state.
Не кожне значення форми потрібно негайно відправляти на сервер. Чернетка, стан фокуса та локальні помилки валідації залишаються відповідальністю клієнта.
Один стан може впливати на отримання іншого.
Наприклад, selectedUserId є client state, але він може визначати, які дані потрібно завантажити:
selectedUserId = 42
↓
запит /users/42/orders
↓
orders = server stateУ такій ситуації:
вибраний ідентифікатор належить клієнту;
список замовлень належить серверу;
зміна selectedUserId може спричинити новий запит.
Це не означає, що замовлення стають client state. Компонент лише використовує клієнтське значення, щоб визначити запит до сервера.
Використовуйте локальний стан, якщо значення потрібне лише одному компоненту або невеликій частині дерева:
відкритість модального вікна;
активна вкладка;
значення поля;
вибраний елемент.
Для цього підходять useState або useReducer.
Якщо кільком компонентам потрібно керувати одним UI-значенням, стан можна підняти до спільного батьківського компонента або передати через Context.
Наприклад:
поточний режим відображення;
стан бокової панелі;
локальний майстер із кількома кроками.
Спосіб поширення стану не змінює його тип. Client state залишається client state, навіть якщо його передають через багато компонентів.
Дані сервера потрібно розглядати з урахуванням:
джерела даних;
повторного використання в різних компонентах;
актуальності;
кешування;
повторних запитів;
синхронізації після змін.
Для простих випадків достатньо useEffect і useState. У більших застосунках спеціалізовані бібліотеки для server state можуть спростити кешування, повторне завантаження та синхронізацію. Проте вони не змінюють головного принципу: джерелом істини залишається сервер.
Якщо зберігати server state і client state без чіткого розділення, виникають проблеми:
дані в UI можуть бути застарілими;
кілька копій одного об’єкта можуть розійтися;
незрозуміло, яку копію потрібно оновити;
локальна зміна може помилково виглядати як успішно збережена;
складніше обробляти помилки сервера;
повторне завантаження може перезаписати незбережені локальні зміни.
Наприклад, після редагування профілю не слід вважати нове ім’я остаточно збереженим лише тому, що локальний стан оновився. Остаточним результатом воно стає після успішної відповіді сервера.
useState — це лише механізм зберігання значення в компоненті. Він не визначає походження даних.
const [user, setUser] = useState(null);Якщо user отримано з API, це server state, навіть якщо він зберігається через useState.
Якщо один і той самий користувач зберігається окремо в кількох компонентах, копії можуть стати різними.
Краще мати зрозуміле джерело server state та оновлювати його узгоджено.
Значення на кшталт isModalOpen не потрібно відправляти в API, якщо воно не має бізнесового значення. Це локальна відповідальність інтерфейсу.
Після setUser(updatedUser) клієнтська копія змінилася, але сервер міг відхилити запит. Підтверджувати server state потрібно результатом успішної серверної операції.
Помилка Не вдалося отримати замовлення походить від роботи із server state.
Помилка Пароль має містити щонайменше 8 символів стосується client state форми.
Таке розділення допомагає показувати користувачу правильний тип повідомлення та правильно повторювати операцію.
Перед додаванням нового стану до компонента:
Визначте джерело даних.
З’ясуйте, чи повинні ці дані зберігатися після перезавантаження сторінки.
Визначте, чи потрібен API-запит.
Перевірте, чи є значення тимчасовим станом UI.
Вирішіть, хто має бути власником стану.
Для server state передбачте завантаження, помилку та актуальність.
Для client state визначте подію, яка змінює значення.
Коротке правило:
Якщо дані належать серверу — це server state. Якщо дані описують поточну поведінку інтерфейсу — це client state.
Server state — дані, джерелом істини для яких є сервер.
Client state — дані, якими керує інтерфейс у браузері.
Дані, отримані через API, залишаються server state, навіть якщо зберігаються в useState.
Відкритість модального вікна, активна вкладка та значення незбереженої форми — приклади client state.
Один компонент може одночасно використовувати обидва типи стану.
Для server state важливі завантаження, помилки, актуальність і синхронізація.
Для client state важливі локальна поведінка та реакція на дії користувача.
Чіткий поділ відповідальності зменшує дублювання, розбіжності даних і помилки під час оновлення інтерфейсу.