Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Підходи до управління станом у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Управління станом у React** (state management) - це система зберігання та передачі даних між компонентами: від `useState` до глобальних сторів і серверних кешів. ```tsx const [count, setCount] = useState(0); // локальний const cartCount = useCartStore(s => s.items.length); // глобальний, селектор ``` **Головне:** починай з `useState`, для shared UI-стану - Zustand, для даних з API - TanStack Query.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Управління станом у React** (state management) - це система зберігання, оновлення та передачі даних між компонентами: від одного виклику `useState` до глобального стору або серверного кешу. ## Теорія ### Коротко - `useState` - особиста полиця: швидко, локально, нуль налаштувань - Context - спільний холодильник: доступний всій app, але кожен споживач ре-рендериться при зміні - Zustand покриває 90% кейсів із глобальним станом з мінімальним кодом (+3KB) - TanStack Query - для серверного стану: кешування, фоновий рефетч, мутації - Правило вибору: менше 3 компонентів ділять дані? `useState`. Складна асинхронність? TanStack Query. Глобальний UI? Zustand. ### Швидкий приклад ```tsx // Локальний стан: компонент зберігає свій лічильник function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; } // Спільний стан: тема доступна будь-де без prop drilling const ThemeContext = createContext<"light" | "dark">("light"); function App() { const [theme, setTheme] = useState<"light" | "dark">("light"); return ( <ThemeContext.Provider value={theme}> <Child /> {/* читає тему через useContext, без пропсів */} </ThemeContext.Provider> ); } ``` `useState` ре-рендерить лише свій компонент. Context ре-рендерить всіх споживачів. Ця різниця відчутна при масштабуванні. ### Головний поділ Стан у React ділиться на три категорії за областю видимості. **Локальний стан** (`useState`, `useReducer`) живе в одному компоненті й тригерить ре-рендер тільки там. **Спільний стан** (Context, Zustand, Redux) поширює зміни по дереву для таких даних як авторизація або кошик. **Серверний стан** (TanStack Query, SWR) - це окрема категорія: вона відповідає за асинхронні запити, кешування та фонову синхронізацію, що не має стосунку до UI-стану. Змішування цих категорій - найпоширеніша проблема в кодових базах. Команда кладе відповіді API у Redux, а потім дивується, чому інвалідація кешу така болюча. ### Коли що використовувати - **Один компонент** (перемикач, інпут, модальне вікно) → `useState`, без варіантів - **Складна локальна логіка** (багатокроковий флоу, undo-історія) → `useReducer` з типізованим union type дій - **2-5 компонентів, що діляться рідко змінюваними даними** (тема, локаль, токен авторизації) → Context API - **Глобальний стан, що часто змінюється** (кошик, налаштування, сповіщення) → Zustand для мінімального API або Redux Toolkit для великих команд із строгими конвенціями - **Дані з API** (список користувачів, результати пошуку, пагінація) → TanStack Query або SWR - **Складні форми з валідацією** → React Hook Form, uncontrolled-підхід без ре-рендеру на кожен символ - **Фільтри, пагінація, сортування** → URL params через `URLSearchParams`, зберігаються після перезавантаження і шаряться посиланням ### Таблиця порівняння | Підхід | Область | Шаблонний код | Async | Вага | Для чого | |---|---|---|---|---|---| | `useState` | Локальний | Відсутній | Вручну | 0 | UI перемикачі, інпути | | `useReducer` | Локальний/shared | Мінімальний | Вручну | 0 | Складна локальна логіка | | Context API | App-wide | Мінімальний | Вручну | 0 | Тема, auth, локаль | | Zustand | Глобальний | Мінімальний | Через плагіни | +3KB | Більшість додатків | | Redux Toolkit | Глобальний | Середній | RTK Query | +12KB | Великі команди, legacy | | TanStack Query | Серверний | Мінімальний | Вбудований кеш | +14KB | API-heavy додатки | | React Hook Form | Форми | Мінімальний | N/A | +9KB | Валідація | ### Як це працює зсередини React ставить оновлення стану в чергу у fiber-вузлах. Кожен виклик `useState` підключається до диспетчера й прив'язує чергу оновлень до індексу хука. У React 18 оновлення всередині обробників подій автоматично пакетуються, тому кілька викликів `setState` дають один ре-рендер замість кількох. Context Provider створює обхідник дерева. Коли значення контексту змінюється, React обходить піддерево й позначає компоненти-споживачі для ре-рендеру. Вбудованого механізму селекторів немає: якщо об'єкт-значення змінює посилання, всі споживачі ре-рендеряться. Саме тому великий об'єкт у Context без мемоізації - це проблема продуктивності. TanStack Query використовує глобальний `Map` з ключами запитів. Запити проходять через `AbortController`, тому навігація зі сторінки скасовує незавершені запити. Visibility API тригерить фоновий рефетч, коли користувач повертається у вкладку. Все це працює повністю поза моделлю стану React. ### Типові помилки **Піднімати весь стан нагору й передавати пропсами вниз.** ```tsx // Неправильно: кожен дочірній компонент ре-рендериться при будь-якій зміні кошика function App() { const [cart, setCart] = useState([]); return <Header cart={cart} />; // Header ре-рендериться навіть якщо показує тільки кількість } // Правильно: селектор, підписуємось тільки на те що потрібно const cartCount = useCartStore(state => state.items.length); // Ре-рендер тільки при зміні кількості, не при будь-якій мутації кошика ``` **Мутувати стан напряму.** ```tsx // Неправильно: React не бачить змін, ре-рендеру немає const addItem = () => { cart.push(newItem); setCart(cart); // те саме посилання, React ігнорує }; // Правильно: нове посилання на масив setCart([...cart, newItem]); ``` **Не вказувати залежності у ключах TanStack Query.** ```tsx // Неправильно: кеш не оновлюється при зміні userId useQuery({ queryKey: ["posts"], queryFn: () => fetchPosts(userId) }); // Правильно: userId входить у ключ useQuery({ queryKey: ["posts", userId], queryFn: () => fetchPosts(userId) }); ``` **Класти все в один Context.** Коли один Context містить забагато, будь-яка зміна ре-рендерить все піддерево. Розбивай на менші, фокусовані Contexts або переходь на Zustand. ### Де зустрічається в реальних проектах - Next.js додатки на Vercel використовують TanStack Query для fetching'у з паттерном stale-while-revalidate - Shopify admin використовує Redux Toolkit зі структурованими слайсами для складного стану e-commerce - Discord web-клієнт використовує Zustand для легкого глобального UI-стану - Stripe checkout використовує React Hook Form для валідації без ре-рендеру на кожен символ - Більшість команд, що спочатку беруть Redux, після рефакторингу переходять на Zustand ### Follow-up питання **Q:** Коли `useState` спричиняє проблеми з продуктивністю? **A:** Коли той самий стан передається через глибоке дерево пропсами. Кожен `setState` тригерить ре-рендер у кожному споживачі. Вирішується переходом на Context з мемоізацією або на Zustand-селектори. **Q:** В чому різниця між Zustand і Redux Toolkit? **A:** Redux дає більше структури: actions, reducers, slices, строгі конвенції, що допомагають великим командам залишатись послідовними. Zustand досягає того самого з набагато меншою кількістю коду. Для команди з 2-5 розробників Zustand - майже завжди правильний вибір. **Q:** Як TanStack Query запобігає request waterfalls? **A:** Паралельні запити через suspense-межі й `prefetchQuery` на рівні роутера, тому дані завантажуються до того, як компоненти монтуються. **Q:** Що не так із `useReducer` плюс Context для великих додатків? **A:** Без селекторів будь-яка зміна значення контексту ре-рендерить усе дерево. Вбудованого способу підписатися на частину контексту немає. Zustand вирішує це через `useStore(selector)`. **Q:** Як transitions у React 18 впливають на оновлення стану? **A:** `useTransition` позначає оновлення як низькопріоритетні. Важкий фільтр, позначений як transition, залишає інпут відзивчивим, поки результати оновлюються у фоні. **Q:** Опиши optimistic updates у TanStack Query. **A:** У `useMutation` встановлюєш `onMutate` для негайного оновлення кешу і зберігаєш попереднє значення, потім викликаєш `onError` для відкату, якщо сервер повертає помилку. ## Приклади ### Локальний стан: контрольований інпут ```tsx function SearchInput() { const [query, setQuery] = useState(""); return ( <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Пошук..." /> ); } // Кожен символ оновлює query, тільки цей компонент ре-рендериться ``` Контрольовані інпути - найпоширеніший випадок для `useState`. Компонент тримає значення, React контролює цикл рендеру. ### Глобальний стан: Zustand-стор для кошика ```tsx import { create } from "zustand"; interface CartItem { id: number; qty: number; price: number; } interface CartStore { items: CartItem[]; addItem: (item: CartItem) => void; removeItem: (id: number) => void; total: () => number; } export const useCartStore = create<CartStore>((set, get) => ({ items: [], addItem: (item) => set(state => ({ items: [...state.items, item] })), removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id), })), total: () => get().items.reduce((sum, i) => sum + i.price * i.qty, 0), })); // Ре-рендер тільки при зміні кількості елементів, не при зміні цін function CartBadge() { const count = useCartStore(state => state.items.length); return <span className="badge">{count}</span>; } ``` Селектор `state => state.items.length` - ось що робить Zustand продуктивним тут. `CartBadge` ре-рендериться тільки при зміні кількості, а не при оновленні цін чи кількості товарів. ### Серверний стан: TanStack Query з мутацією ```tsx import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; function UserList() { const queryClient = useQueryClient(); const { data: users, isLoading } = useQuery({ queryKey: ["users"], queryFn: () => fetch("/api/users").then(r => r.json()), }); const deleteUser = useMutation({ mutationFn: (id: number) => fetch(`/api/users/${id}`, { method: "DELETE" }), onSuccess: () => { // Позначає кеш як застарілий, тригерить автоматичний рефетч queryClient.invalidateQueries({ queryKey: ["users"] }); }, }); if (isLoading) return <p>Завантаження...</p>; return ( <ul> {users.map(user => ( <li key={user.id}> {user.name} <button onClick={() => deleteUser.mutate(user.id)}>Видалити</button> </li> ))} </ul> ); } ``` `invalidateQueries` після мутації позначає кеш як застарілий і тригерить фоновий рефетч. Жодного `useEffect` вручну, жодного жонглювання станом.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.