Питання на співбесіді: Стан і дані
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
12 питань
Коли два компоненти мають показувати чи змінювати одні й ті самі дані, стан не може жити в кожному з них окремо - копії розійдуться. Його переносять у найближчого спільного предка й передають вниз через props.
Було - кожна панель сама вирішує, чи відкрита:
function Panel({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
// ...
}
Вимога «відкрита лише одна панель одночасно» так не реалізується: панелі не знають одна про одну.
Стало - стан у батька, панелі керовані:
function Accordion() {
const [openId, setOpenId] = useState('about');
return (
<>
<Panel title="Про нас" isOpen={openId === 'about'} onOpen={() => setOpenId('about')}>...</Panel>
<Panel title="Доставка" isOpen={openId === 'delivery'} onOpen={() => setOpenId('delivery')}>...</Panel>
</>
);
}
function Panel({ title, isOpen, onOpen, children }) {
return (
<section>
<button onClick={onOpen}>{title}</button>
{isOpen && children}
</section>
);
}
Panel став керованим (controlled): ним керує батько через props. Компонент зі своїм внутрішнім станом - некерований (uncontrolled).
Принцип «одного джерела правди»: для кожного шматка стану є один компонент-власник. Інші отримують значення через props і повідомляють про зміни через колбеки.
Куди піднімати:
- до найближчого спільного предка всіх, хто використовує дані, - не вище. Стан занадто високо змушує перерендерюватися велику частину дерева й ускладнює компоненти;
- якщо спільний предок дуже далеко і дані треба передавати через багато рівнів («prop drilling»), - спершу композиція (передати готовий JSX через
children), потім Context.
Зворотний процес теж корисний: якщо стан використовується лише в одному компоненті, його варто опустити туди (state colocation) - менше зайвих рендерів і простіші батьки.
Типова помилка - дублювати стан: копіювати props у локальний стан дитини (useState(props.value)). Копія не оновиться, коли батько змінить значення. Або компонент керований (значення з props), або некерований (власний стан з початковим значенням), - змішування дає розсинхронізацію.
Надлишковий стан - значення, яке можна обчислити з props чи іншого стану. Його зберігання - джерело помилок, бо дві копії правди рано чи пізно розходяться.
Погано:
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // надлишкове
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName); // треба не забути оновити тут
}
Добре - обчислювати під час рендеру:
const fullName = `${firstName} ${lastName}`;
Типові випадки надлишкового стану:
- відфільтрований чи відсортований список поруч з оригіналом:
const visibleTodos = todos.filter((t) => (showDone ? true : !t.done));
- кількості й суми:
items.length,items.reduce(...); - обраний елемент як копія об'єкта - краще зберігати
selectedIdі знаходити об'єкт:items.find((i) => i.id === selectedId). Інакше зміна елемента в списку не потрапить у «обраний»; - прапорці, що випливають з даних:
isEmpty,hasErrors,canSubmit.
Антипатерн - синхронізація ефектом:
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(todos.filter(...));
}, [todos]);
Зайвий рендер зі старими даними, потім ще один - з новими. Плюс місце для помилки, якщо забути залежність.
А якщо обчислення дороге? Спершу виміряти. Якщо справді повільно - useMemo (або React Compiler зробить це автоматично):
const visibleTodos = useMemo(() => filterTodos(todos, query), [todos, query]);
Коли копія з props доречна: коли це початкове значення, яке далі живе своїм життям, - і тоді props варто назвати відповідно (initialColor, defaultValue), щоб було зрозуміло, що подальші зміни props не враховуються.
Перевірка структури стану: для кожного значення в useState запитати: «чи можна його обчислити з інших?». Якщо так - це не стан.
Найпростіше завантаження в ефекті:
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 для даних лишається прийнятним для невеликих застосунків і простих випадків - але з очищенням.
Докладніше в документації: Синхронізація з ефектами: завантаження даних
Для стану, яким користуються компоненти на різних рівнях дерева (кошик, список завдань, налаштування), React пропонує поєднання useReducer + Context без сторонніх бібліотек.
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, []);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}
export function useTasks() {
const tasks = useContext(TasksContext);
if (tasks === null) throw new Error('useTasks має бути всередині TasksProvider');
return tasks;
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
У React 19 сам контекст можна рендерити як провайдер (<TasksContext value={...}>); старий запис <TasksContext.Provider> теж працює.
Використання в будь-якому компоненті під провайдером:
function AddTask() {
const dispatch = useTasksDispatch();
return <button onClick={() => dispatch({ type: 'added', text: 'Нове завдання' })}>Додати</button>;
}
function TaskList() {
const tasks = useTasks();
return tasks.map((task) => <Task key={task.id} task={task} />);
}
Чому два контексти, а не один:
dispatchстабільний - не змінюється між рендерами;- компоненти, яким потрібна лише відправка дій (кнопки, форми), підписуються лише на
TasksDispatchContextі не перерендерюються, коли змінюється список завдань.
Переваги підходу:
- логіка змін - в одному редукторі, її легко тестувати;
- компоненти не передають колбеки через десятки рівнів;
- власні хуки (
useTasks) приховують деталі й перевіряють наявність провайдера.
Обмеження, через які переходять на сховища:
- будь-яка зміна значення контексту перерендерює всіх його споживачів - навіть тих, кому потрібна лише частина даних. Немає селекторів «підпишись лише на це поле»;
- провайдери, що вкладаються один в один для кожної частини стану, ускладнюють структуру;
- немає інструментів налагодження «з коробки», проміжних обробників (middleware), збереження стану.
Для частих оновлень і великого спільного стану - Zustand, Redux Toolkit чи подібні з підписками на частину стану.
Докладніше в документації: Масштабування з reducer і context
Клієнтський стан належить інтерфейсу: відкрите модальне вікно, текст у полі, обрана вкладка. Він живе в браузері, і застосунок - його єдиний власник.
Серверний стан - копія даних, що живуть на сервері: список замовлень, профіль, вакансії. Його особливості:
- може змінитися без участі вашого застосунку (інший користувач, фонова задача);
- потребує завантаження, кешування, повторних запитів, обробки помилок;
- може бути застарілим у будь-який момент.
Зберігати його в useState чи глобальному сторі - означає вручну писати кеш, стани завантаження, інвалідацію й дедуплікацію запитів.
TanStack Query бере це на себе:
import { useQuery } from '@tanstack/react-query';
function Orders({ status }) {
const { data, isPending, error } = useQuery({
queryKey: ['orders', { status }],
queryFn: () => fetch(`/api/orders?status=${status}`).then((r) => r.json()),
staleTime: 30_000,
});
if (isPending) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <OrderList orders={data} />;
}
Що дає:
- кеш за ключем (
queryKey): повернення на сторінку показує дані миттєво, а оновлення йде у фоні; - дедуплікація: десять компонентів з тим самим ключем - один запит;
- stale-while-revalidate: застарілі дані показуються, поки завантажуються свіжі;
- повторні запити при поверненні на вкладку, відновленні мережі, з інтервалом;
- повтори при помилках з затримкою;
- мутації (
useMutation) з інвалідацією кешу й оптимістичними оновленнями; - скасування застарілих запитів і відсутність гонитви.
Ключові налаштування:
staleTime- скільки даних вважаються свіжими (за замовчуванням 0 - застарілі одразу, тож повторний запит при кожному монтуванні). Для більшості даних варто задати секунди чи хвилини;gcTime- скільки невикористовувані дані лишаються в кеші;- ключ має містити всі параметри запиту - інакше різні запити ділитимуть один кеш.
Розподіл відповідальності: серверний стан - TanStack Query (чи SWR, RTK Query, завантажувачі фреймворку), клієнтський - useState, Context чи невеликий стор. Багато застосунків після переходу виявляють, що глобальний стор їм майже не потрібен.
Context - механізм передачі значення вниз по дереву, а не повноцінне керування станом. Його головне обмеження: зміна значення перерендерює всіх споживачів, навіть якщо їм потрібна лише частина даних.
Зовнішній стор тримає стан поза деревом React, а компоненти підписуються на окремі частини через селектори:
// Zustand
const useCartStore = create((set) => ({
items: [],
add: (item) => set((state) => ({ items: [...state.items, item] })),
}));
function CartBadge() {
const count = useCartStore((state) => state.items.length); // лише кількість
return <span>{count}</span>;
}
CartBadge перерендериться лише коли зміниться кількість, а не будь-що в сторі.
// Redux Toolkit
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
added(state, action) {
state.items.push(action.payload); // Immer усередині - виглядає як мутація, але незмінно
},
},
});
Коли зовнішній стор виправданий:
- часті оновлення спільного стану (редактор, канвас, дашборд у реальному часі) - селектори відсікають зайві рендери;
- багато компонентів у різних частинах дерева читають і змінюють той самий стан;
- складна логіка змін, яку зручно тримати поза компонентами й тестувати окремо;
- доступ поза React: з обробників WebSocket, сервісів, роутера;
- інструменти: Redux DevTools (історія дій, «подорож у часі»), збереження стану, middleware.
Коли не потрібен:
- дані з сервера - для них краще TanStack Query чи RTK Query, а не ручне зберігання в сторі;
- стан однієї сторінки чи компонента -
useState/useReducer; - рідко змінювані значення (тема, мова, поточний користувач) - Context цілком достатній.
Порівняння бібліотек:
- Redux Toolkit - структурований, з DevTools і RTK Query, більше коду, але передбачуваний для великих команд;
- Zustand - мінімальний API, хук-стор без провайдера;
- Jotai - атомарний підхід: стан з дрібних незалежних атомів.
Усі вони побудовані на useSyncExternalStore - безпечні для конкурентного рендерингу.
Пастка селекторів: селектор, що повертає новий об'єкт ((s) => ({ a: s.a, b: s.b })), щоразу дає «інше» значення і перерендерює компонент. Потрібні окремі селектори чи порівняння shallow.
Стан каталогу (фільтри, пошук, сортування, сторінка) у useState зникає при оновленні сторінки, не працює з кнопкою «Назад» і не передається посиланням. URL - природне місце для такого стану.
Що дає стан в URL:
- посилання відтворює екран: «ось вакансії PHP у Києві, віддалено» - одне посилання;
- «Назад» і «Вперед» повертають попередні фільтри;
- оновлення сторінки нічого не губить;
- серверний рендер і SEO: сервер бачить параметри й рендерить правильний вміст;
- аналітика бачить, як саме користуються фільтрами.
З React Router:
import { useSearchParams } from 'react-router';
function Vacancies() {
const [searchParams, setSearchParams] = useSearchParams();
const city = searchParams.get('city') ?? 'all';
const page = Number(searchParams.get('page') ?? 1);
function changeCity(nextCity) {
setSearchParams((params) => {
params.set('city', nextCity);
params.delete('page'); // новий фільтр - перша сторінка
return params;
});
}
// ...
}
Значення читаються з URL під час рендеру - окремий useState для них не потрібен. URL і є джерелом правди.
Без роутера - URLSearchParams + history.replaceState/pushState і підписка на popstate (через useSyncExternalStore).
Що варто врахувати:
pushчиreplace: зміна фільтра - новий запис історії (користувач повертається «Назад» до попередніх фільтрів); введення в поле пошуку -replace, щоб не засмічувати історію кожною літерою;- debounce для полів введення: не змінювати URL на кожне натискання;
- валідація: параметри з URL - дані від користувача. Невідоме значення сортування чи від'ємна сторінка мають оброблятися без падіння;
- типи: усе в URL - рядки; числа й булеві значення треба перетворювати (бібліотеки на кшталт
nuqsроблять це з типами); - короткі й стабільні назви параметрів - вони стають частиною публічних посилань.
Що НЕ варто тримати в URL: тимчасовий стан інтерфейсу (відкрите меню, наведення), персональні дані, великі обсяги даних.
Для Laravel-бекенду формат на кшталт tags[]=php&tags[]=vue зручний: сервер отримає масив.
Збереження стану між сесіями (тема, згорнута бічна панель, чернетка форми) - типова задача. Найпростіший варіант:
function useLocalStorageState(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// сховище заповнене чи недоступне - стан працює і без збереження
}
}, [key, value]);
return [value, setValue];
}
Лінива ініціалізація (функція в useState) - читання з localStorage лише один раз, а не на кожному рендері.
Пастки:
1. Серверний рендер. На сервері localStorage не існує - код вище впаде з ReferenceError. А якщо перевіряти typeof window, сервер відрендерить початкове значення, клієнт - збережене, і React повідомить про розбіжність гідратації.
Правильно - на першому рендері використовувати те саме значення, що й сервер, і читати сховище після монтування; або useSyncExternalStore з getServerSnapshot, що повертає значення за замовчуванням. Для теми, щоб не було «спалаху», - невеликий скрипт у <head>, що ставить клас до запуску React, або cookie, яку бачить і сервер.
2. Синхронізація між вкладками. Зміни в одній вкладці не видно в іншій без підписки на подію storage. useSyncExternalStore з підпискою на неї вирішує це.
3. Зміна формату даних. Нова версія застосунку очікує інший формат, а в браузерах лежать старі дані. Додавайте версію в ключ (settings:v2) чи валідацію при читанні (Zod) - некоректні дані замінюються значенням за замовчуванням.
4. Винятки. localStorage кидає помилки в приватному режимі, при заповненому сховищі (~5 МБ), при заборонених cookie - доступ обгортається в try/catch.
5. Безпека. Будь-який скрипт на сторінці читає localStorage - туди не кладуть токени автентифікації й персональні дані.
6. Продуктивність. setItem синхронний; запис великого об'єкта на кожне натискання клавіші - помітні затримки. Для чернеток - debounce.
Готові рішення: useLocalStorage з бібліотек хуків, middleware persist у Zustand, redux-persist - вони вже враховують частину цих проблем.
Найпідступніші помилки стану - неможливі комбінації, які структура даних дозволяє, а логіка - ні.
1. Суперечливі прапорці → одне поле статусу:
// погано: що означає isLoading && isError? а isSuccess && isError?
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
// добре: рівно один стан у кожен момент
const [status, setStatus] = useState('idle'); // 'idle' | 'loading' | 'error' | 'success'
Ще краще - дискримінований тип, де дані існують лише в «правильних» станах:
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error'; error: string }
| { status: 'success'; data: Order[] };
TypeScript не дасть прочитати data в стані error.
2. Дублювання → ідентифікатори:
// погано: копія об'єкта - зміна в списку не потрапить у selectedItem
const [selectedItem, setSelectedItem] = useState(items[0]);
// добре
const [selectedId, setSelectedId] = useState(items[0].id);
const selectedItem = items.find((item) => item.id === selectedId);
3. Глибока вкладеність → нормалізація. Дерево (категорії з підкатегоріями, коментарі з відповідями) зручно відображати, але незручно оновлювати: зміна глибокого вузла вимагає копіювати весь шлях. Нормалізована форма - як таблиці в базі:
{
commentsById: {
1: { id: 1, text: '...', childIds: [2, 3] },
2: { id: 2, text: '...', childIds: [] },
},
rootIds: [1],
}
Оновлення одного коментаря - одна заміна за id. Redux Toolkit має для цього createEntityAdapter.
4. Групування пов'язаного стану: значення, що завжди змінюються разом (координати x і y, поля однієї форми), - в одному об'єкті чи редукторі, а не в окремих useState, які легко оновити неузгоджено.
5. Надлишковий стан → обчислення під час рендеру.
Перевірка структури: для кожної комбінації значень у стані запитати - чи вона можлива в реальності? Якщо ні, структуру варто змінити так, щоб її неможливо було виразити. «Зробити неможливі стани невиразними» - головна ідея.
Для складних сценаріїв (багатокрокові форми, процеси оплати, завантаження з повторами) - явний скінченний автомат: редуктор з переходами або бібліотека XState.
Оптимістичне оновлення - показати результат дії одразу, не чекаючи відповіді сервера, і відкотити, якщо сервер повернув помилку. Інтерфейс відчувається миттєвим: «лайк», позначка завдання, перейменування.
Варіант через кеш запиту:
const queryClient = useQueryClient();
const toggleTodo = useMutation({
mutationFn: (todo) =>
fetch(`/api/todos/${todo.id}`, { method: 'PATCH', body: JSON.stringify({ done: !todo.done }) }),
onMutate: async (todo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] }); // 1
const previous = queryClient.getQueryData(['todos']); // 2
queryClient.setQueryData(['todos'], (old) => // 3
old.map((t) => (t.id === todo.id ? { ...t, done: !t.done } : t)),
);
return { previous };
},
onError: (error, todo, context) => {
queryClient.setQueryData(['todos'], context.previous); // 4
toast.error('Не вдалося зберегти');
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }), // 5
});
Кроки й навіщо кожен:
- скасувати поточні запити списку - інакше відповідь, що вже летить, перезапише оптимістичні дані старими;
- зберегти знімок для відкату;
- оновити кеш - інтерфейс змінюється миттєво;
- при помилці - відкотити до знімка і повідомити користувача;
- після завершення - перезапитати дані з сервера, щоб кеш точно відповідав реальності.
Простіший варіант - через змінні мутації: не чіпати кеш, а в компоненті показувати mutation.variables, поки мутація в стані isPending. Менше коду, відкат автоматичний (просто зникає тимчасове відображення), але зміна видна лише там, де рендериться мутація.
React 19 useOptimistic - схожа ідея для дій і форм без бібліотеки: тимчасовий стан, що діє до завершення асинхронної дії.
Коли оптимізм доречний: дія майже завжди успішна, легко відкочується і не має незворотних наслідків.
Коли ні: оплати, відправка листів, видалення без можливості відновлення, операції з високим шансом відмови через валідацію - там краще чесний стан «Зберігаємо...».
Пастки:
- кілька швидких мутацій однієї сутності: відкат однієї може затерти іншу - враховувати це при розробці
onError(чи серіалізувати мутації); - згенеровані сервером поля (id, дати) в оптимістичному записі - тимчасові значення, які замінить відповідь;
- повідомлення про відкат обов'язкове - тихе «повернення» стану користувач сприйме як баг.
Докладніше в документації: TanStack Query: оптимістичні оновлення
Документація React пропонує думати про інтерфейс декларативно: не «що змінити після кліку», а «які візуальні стани бувають і що переводить з одного в інший».
Процес:
- перелічити візуальні стани: форма відповіді на питання -
empty(поле порожнє),typing,submitting,error,success; - визначити, що їх змінює: введення тексту, натискання «Надіслати», відповідь сервера (успіх чи помилка);
- описати стан мінімальним набором змінних без суперечностей;
- підключити обробники, що змінюють стан.
Редуктор як скінченний автомат:
function quizReducer(state, event) {
switch (state.status) {
case 'typing':
if (event.type === 'submit') return { ...state, status: 'submitting' };
if (event.type === 'change') return { ...state, answer: event.value };
return state;
case 'submitting':
if (event.type === 'resolved') return { ...state, status: 'success' };
if (event.type === 'rejected') return { ...state, status: 'typing', error: event.error };
return state; // під час відправки зміна тексту ігнорується
case 'success':
return state; // кінцевий стан
default:
return state;
}
}
Головна відмінність від звичайного редуктора - перевірка поточного стану перед обробкою події. Подія, недопустима в цьому стані, просто ігнорується: подвійне натискання «Надіслати» під час відправки не створить другий запит.
Що це дає:
- неможливі переходи неможливі: не можна «надіслати» з
success, змінити текст під часsubmitting; - інтерфейс - функція від стану:
disabled={state.status === 'submitting'}, повідомлення про успіх лише вsuccess; - легко тестувати: редуктор - чиста функція, перелік станів і переходів можна перевірити повністю;
- легко побачити всі стани: для кожного з них можна відрендерити компонент окремо (наприклад, у Storybook) і перевірити дизайн.
Коли брати бібліотеку (XState): паралельні й вкладені стани, таймери й затримки як частина логіки, складні багатокрокові процеси (онбординг, оформлення замовлення з кількома способами оплати), потреба візуалізувати автомат для команди.
Для простих компонентів досить одного поля status замість кількох булевих прапорців - це вже більша частина користі.
Кеш серверних даних корисний, доки він актуальний. Є три джерела змін, і кожне потребує свого механізму.
1. Зміни, зроблені самим користувачем (мутації). Після успішного запиту кеш, що залежить від змінених даних, треба оновити:
useMutation({
mutationFn: createOrder,
onSuccess: (order) => {
queryClient.invalidateQueries({ queryKey: ['orders'] }); // позначити застарілим і перезапитати
queryClient.setQueryData(['orders', order.id], order); // покласти відповідь у кеш одразу
},
});
invalidateQueries з префіксом ключа (['orders']) зачіпає всі запити, ключ яких з нього починається: списки з різними фільтрами, сторінки пагінації. Тому ієрархічні ключі (['orders', { status }], ['orders', id]) важливі з самого початку.
2. Зміни, зроблені іншими (інші користувачі, фонові задачі).
- повторний запит при поверненні на вкладку (
refetchOnWindowFocus, увімкнено за замовчуванням) і з інтервалом (refetchInterval) - простий варіант без інфраструктури; - події в реальному часі - WebSocket чи SSE (у Laravel - Reverb + Echo). Подія від сервера інвалідує чи оновлює кеш:
useEffect(() => {
const channel = echo.private(`team.${teamId}`);
channel.listen('OrderShipped', (event) => {
queryClient.setQueryData(['orders', event.order.id], event.order);
queryClient.invalidateQueries({ queryKey: ['orders'], exact: false, refetchType: 'active' });
});
return () => echo.leave(`team.${teamId}`);
}, [teamId, queryClient]);
setQueryData чи invalidateQueries:
setQueryData- коли подія містить повні актуальні дані: без додаткового запиту;invalidateQueries- коли подія лише сигналізує «щось змінилося»: менше даних у повідомленні, але запит на сервер.refetchType: 'active'перезапитує лише те, що зараз на екрані, решта оновиться при наступному використанні.
3. Застарівання за часом - staleTime. Для кожного типу даних - свій: довідники (міста, категорії) - години, список замовлень - секунди, курс валют - хвилина.
Пастки:
- шторм запитів: подія, що інвалідує широкий ключ, у тисячі відкритих вкладок одночасно - тисячі запитів на сервер. Варто передавати дані в самій події або додавати випадкову затримку;
- порядок подій: подія може прийти раніше, ніж відповідь на мутацію в цій самій вкладці, - перевіряти версію (
updated_at) перед записом у кеш; - авторизація каналів: приватні канали WebSocket мають перевіряти, чи користувач має доступ до даних, - інакше кеш отримає чужі дані.
Докладніше в документації: TanStack Query: інвалідація запитів