Найпростіше завантаження в ефекті:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then((response) => response.json())
.then((data) => {
if (!ignore) setUser(data);
});
return () => {
ignore = true;
};
}, [userId]);
if (!user) return <Spinner />;
return <h1>{user.name}</h1>;
}
Навіщо змінна ignore. Користувач швидко перемикає профілі: userId 1, потім 2. Летять два запити. Якщо відповідь для 1 прийде пізніше за відповідь для 2, без перевірки вона перезапише стан - на екрані профіль 2-го користувача з даними 1-го. Функція очищення попереднього ефекту ставить ignore = true, і застаріла відповідь ігнорується.
Ще краще - скасувати запит:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((r) => r.json())
.then(setUser)
.catch((error) => {
if (error.name !== 'AbortError') setError(error);
});
return () => controller.abort();
}, [userId]);
StrictMode у розробці запускає ефект двічі - з очищенням між запусками. Перший запит скасовується. Це нормально й саме перевіряє, що очищення написане.
Чого бракує цьому підходу (і чому документація React радить не писати таке вручну у великих застосунках):
- немає кешу: повернення на сторінку - новий запит і знову спінер;
- «водоспад» запитів: дочірні компоненти починають завантаження лише після рендеру батька;
- стани помилки й завантаження - у кожному компоненті вручну;
- немає попереднього завантаження і повторного запиту при поверненні на вкладку;
- серверний рендер: ефекти на сервері не виконуються.
Альтернативи:
- фреймворк (Next.js, React Router з завантажувачами) - дані завантажуються до рендеру маршруту;
- TanStack Query чи SWR - кеш, повтори, скасування, синхронізація;
use()з Suspense і промісом зі стабільного джерела.
useEffect для даних лишається прийнятним для невеликих застосунків і простих випадків - але з очищенням.
Докладніше в документації: Синхронізація з ефектами: завантаження даних