Skip to main content

Чому не можна зберігати проміси в стані?

Що означає "зберігати проміс у стані"?

Приклад (неправильний варіант):

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
нестабільні об'єктистабільні значення стану
побічні ефекти в тілі функціїчиста функція + ефект

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.