Самая полезная мысль про состояние в мобильном приложении: серверные данные и клиентское состояние это разные вещи. Список заказов принадлежит серверу, мы держим лишь его копию. Открыт ли фильтр, выбранная вкладка, черновик формы принадлежат клиенту. Путаница между ними порождает половину багов с устаревшими данными.
Клиентское состояние
- Локальное:
useState,useReducerвнутри компонента. - Общее для нескольких экранов: Context для редко меняющихся значений (тема, текущий пользователь), Zustand или Redux Toolkit для активно меняющегося.
Context перерисовывает всех потребителей при каждом изменении значения. Для частых изменений это дорого, поэтому контекст делят на несколько или используют стор с селекторами.
Zustand: маленький стор на хуках, подписка через селектор, перерисовка только при изменении выбранного куска.
const useCart = create<CartState>()((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
const count = useCart((s) => s.items.length);
Redux Toolkit: createSlice с иммутабельными обновлениями через Immer, configureStore, DevTools, строгий однонаправленный поток. Подходит большим командам. RTK Query внутри него закрывает серверные данные.
Серверное состояние
Задачи, которые возникают у любого экрана с данными: загрузка, ошибка, повтор, кеш, обновление при возврате на экран, отмена устаревших запросов, пагинация, оптимистичные обновления. Писать это руками в useEffect долго и с ошибками (гонки ответов, setState после размонтирования).
TanStack Query решает это из коробки:
const { data, isPending, error, refetch } = useQuery({
queryKey: ['orders', status],
queryFn: () => api.orders(status),
staleTime: 60_000,
});
queryKeyэто ключ кеша: разные параметры, разные записи.staleTime: сколько данные считаются свежими. Пока свежие, повторного запроса нет.gcTime: сколько неиспользуемые данные живут в кеше.useInfiniteQueryдля пагинации: страницы,getNextPageParam,fetchNextPage.useMutationсonMutateдля оптимистичного обновления и откатом вonError.
В React Native нужно подключить два платформенных сигнала: focusManager к AppState (обновить данные при возврате приложения из фона) и onlineManager к NetInfo (не слать запросы без сети и повторить, когда сеть появилась).
Пагинация
Два вида:
- offset/limit (
?page=3): просто, но при вставке новых записей на сервере страницы сдвигаются и дают дубли или пропуски; - курсорная (
?after=<id последнего>): стабильна при вставках, стандарт для лент.
На клиенте склеивают страницы и убирают дубли по id, а флаг загрузки не даёт onEndReached запросить одну страницу дважды.
Оптимистичные обновления
Пользователь нажал «лайк»: сразу показываем результат, отправляем запрос, при ошибке откатываем и сообщаем. Нужно сохранить предыдущее состояние, отменить параллельные запросы этого ключа, после ответа сверить с сервером.
Частые ошибки
- Копировать серверные данные в глобальный стор и вручную их синхронизировать.
- Один огромный Context на всё приложение.
- Гонка ответов: старый запрос пришёл позже нового и перезаписал результат.
- Нет обработки ошибок и состояния «пусто».
Что спросят на собесе
- Чем серверное состояние отличается от клиентского.
- Context против Zustand/Redux: когда что и почему Context может тормозить.
- Что делает TanStack Query: ключи,
staleTime, инвалидация. - Offset и курсорная пагинация.
- Как сделать оптимистичное обновление с откатом.