Чому не можна зберігати проміси в стані?
Що означає "зберігати проміс у стані"?
Приклад (неправильний варіант):
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. Проміс створює побічний ефект під час рендера
const [data, setData] = useState(fetch('/api/user').then(r => r.json()));Тут fetch() виконується при кожному рендері,
а рендерів може бути кілька (React 18 робить подвійний рендер у Strict Mode).
У результаті:
- створюються нові проміси при кожному рендері;
- React не може їх зупинити;
- ви отримаєте дубльовані запити і витоки.
3. Не можна безпечно використовувати then всередині рендера
userPromise.then(data => setUser(data)); // побічний ефект у тілі компонентаЦе порушує правило React:
рендер має бути чистою функцією без побічних ефектів.
Асинхронна операція всередині тіла компонента = брудний рендер, React не гарантує порядок, не зможе правильно скасувати чи повторити його при Concurrent Rendering.
4. React не знає, коли проміс завершився
React працює за принципом:
"Коли змінюється
state, потрібно перерендерити компонент."
Але якщо ви зберігаєте сам проміс, а не його результат, React не дізнається, коли він resolve чи reject.
Тобто UI ніколи не оновиться автоматично:
const [userPromise, setUserPromise] = useState(fetch(...));
// навіть після завершення проміса компонент не перерендериться5. Race conditions і застарілі дані
Якщо компонент розмонтується або отримає нові пропси,
старий проміс все одно може завершитися і спробувати оновити state,
викликаючи:
Warning: Can't perform a React state update on an unmounted componentЯк правильно
Варіант 1. useEffect + стан результату
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
Сучасні бібліотеки беруть усе це на себе:
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.
function User({ resource }) {
const user = resource.user.read(); // може кинути проміс
return <p>{user.name}</p>;
}Тут проміс не зберігається в стані вручну,
React керує ним всередині Suspense-механізму.
Підсумок
| Не можна зберігати | Можна зберігати |
|---|---|
сам проміс (Promise) | результат проміса (data, error, loading) |
| асинхронні ефекти в рендері | асинхронні ефекти в useEffect |
| нестабільні об'єкти | стабільні значення стану |
| побічні ефекти в тілі функції | чиста функція + ефект |
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.