Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Чому не можна зберігати проміси в стані?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)Зберігання самого **проміса** в `state` - антипатерн, бо проміс не серіалізується, не детермінований і сам по собі змінюється (resolve/reject) поза React, тож React не може зрозуміти, коли саме він "змінився" і коли треба оновити UI. **Ключове:** замість самого проміса потрібно зберігати результат проміса (`data`, `error`, `loading`), а асинхронний виклик виконувати в `useEffect`, а не в тілі компонента - інакше рендер перестає бути чистою функцією.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення## Що означає "зберігати проміс у стані"? Приклад (неправильний варіант): ```javascript function User() { const [userPromise, setUserPromise] = useState(fetch('/api/user').then(r => r.json())); // намагаємося використати... userPromise.then(user => console.log(user)); return <div>Профіль користувача</div>; } ``` На перший погляд здається, що все гаразд - проміс зберігається в стані, потім можна "дочекатися" результату. Але на практиці це **антипатерн** і **джерело безлічі проблем**. --- ## Чому це погана ідея ### 1. **Проміси - не серіалізовані і не детерміновані** React очікує, що стан (`state`) - це **детерміновані дані**, які можна: - порівнювати (`Object.is(prev, next)`), - повторно використовувати при повторних рендерах, - скидати й відновлювати (наприклад, при time-travel дебагінгу в Redux DevTools). А проміс: - **не має стабільного значення** (його результат з'явиться пізніше); - **змінюється сам по собі** (resolve/reject поза React); - **не можна серіалізувати** (не можна зберегти в JSON чи snapshot). React не може зрозуміти, коли саме він "змінився", тому зберігання проміса в `state` порушує саму ідею реактивності. --- ### 2. **Проміс створює побічний ефект під час рендера** ```javascript const [data, setData] = useState(fetch('/api/user').then(r => r.json())); ``` Тут `fetch()` виконується **при кожному рендері**, а рендерів може бути кілька (React 18 робить подвійний рендер у Strict Mode). У результаті: - створюються **нові проміси при кожному рендері**; - React не може їх зупинити; - ви отримаєте **дубльовані запити** і **витоки**. --- ### 3. **Не можна безпечно використовувати** `then` **всередині рендера** ```javascript userPromise.then(data => setUser(data)); // побічний ефект у тілі компонента ``` Це порушує правило React: > рендер має бути **чистою функцією** без побічних ефектів. Асинхронна операція всередині тіла компонента = **брудний рендер**, React не гарантує порядок, не зможе правильно скасувати чи повторити його при Concurrent Rendering. --- ### 4. **React не знає, коли проміс завершився** React працює за принципом: > "Коли змінюється `state`, потрібно перерендерити компонент." Але якщо ви зберігаєте сам **проміс**, а не **його результат**, React не дізнається, коли він **resolve** чи **reject**. Тобто UI **ніколи не оновиться** автоматично: ```javascript const [userPromise, setUserPromise] = useState(fetch(...)); // навіть після завершення проміса компонент не перерендериться ``` --- ### 5. **Race conditions і застарілі дані** Якщо компонент розмонтується або отримає нові пропси, старий проміс все одно може завершитися і спробувати оновити `state`, викликаючи: ```javascript Warning: Can't perform a React state update on an unmounted component ``` --- ## Як правильно ### Варіант 1. `useEffect` + стан результату ```javascript function User({ id }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let active = true; setLoading(true); fetch(`/api/users/${id}`) .then(r => r.json()) .then(data => active && setUser(data)) .finally(() => active && setLoading(false)); return () => { active = false; }; // cleanup }, [id]); if (loading) return <p>Loading...</p>; return <p>{user.name}</p>; } ``` Тут: - проміс **не зберігається в стані**; - ми зберігаємо **результат проміса** (`user`, `loading`, `error`); - React **знає, коли оновити UI**. --- ### Варіант 2. Через **React Query / SWR** Сучасні бібліотеки беруть усе це на себе: ```javascript import { useQuery } from "@tanstack/react-query"; function User({ id }) { const { data, isLoading } = useQuery({ queryKey: ['user', id], queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()) }); if (isLoading) return <p>Loading...</p>; return <p>{data.name}</p>; } ``` React Query: - не зберігає проміс у стані; - кешує результати; - скасовує старі запити; - викликає ререндер, коли дані готові. --- ### Варіант 3. `React.Suspense` (для React 18+) Якщо використовувати Suspense для даних, React сам "чекає" проміс під капотом, але це вже контрольований механізм, а не звичайний `useState`. ```javascript function User({ resource }) { const user = resource.user.read(); // може кинути проміс return <p>{user.name}</p>; } ``` Тут проміс **не зберігається в стані вручну**, React керує ним всередині `Suspense`-механізму. --- ## Підсумок | Не можна зберігати | Можна зберігати | |---|---| | сам проміс (`Promise`) | результат проміса (`data`, `error`, `loading`) | | асинхронні ефекти в рендері | асинхронні ефекти в `useEffect` | | нестабільні об'єкти | стабільні значення стану | | побічні ефекти в тілі функції | чиста функція + ефект |Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.