Доки по разработке
This project is maintained by teniryte
Devtools помогают быстро увидеть всё, что происходит в кеше: статусы запросов, очереди мутаций, пересоздания ключей и частоту пересчётов селекторов. В v5 панель вынесена в @tanstack/react-query-devtools, умеет работать как встроенный виджет или отдельная вкладка и поддерживает тёмную тему, собственные кнопки и динамический импорт.
import.meta.env.DEV, чтобы не утянуть пакет в прод.useIsFetching() / useIsMutating() и сравните с тем, что показывает панель.Sort by status, Show query keys и Show stale — они сразу подсвечивают проблемные запросы.refetch.select, откройте вкладку Observers и посмотрите, сколько компонентов завязано на этот запрос.import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
<App />
{import.meta.env.DEV && (
<ReactQueryDevtools
initialIsOpen={window.location.hash === '#rq'}
position="bottom"
buttonPosition="bottom-right"
panelProps={{ style: { maxHeight: '45vh', boxShadow: '0 -8px 32px rgba(0,0,0,.35)' } }}
buttonProps={{ 'aria-label': 'Toggle React Query Devtools' }}
closeButtonProps={{ style: { opacity: 0.6 } }}
/>
)}
</QueryClientProvider>
position принимает top / bottom, а buttonPosition — любую сторону экрана, что удобно при тестировании адаптивов.#rq позволяет автоматически открывать панель, не трогая код.ReactQueryDevtools в if (typeof window !== 'undefined'), чтобы не ломать билды.const Devtools = import.meta.env.DEV
? lazy(() => import('@tanstack/react-query-devtools').then(m => ({ default: m.ReactQueryDevtools })))
: () => null;
Используйте для уменьшения основного бандла; Suspense здесь подходит, потому что Devtools не критичны.
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools';
export function QueryInspector() {
const queryClient = useQueryClient();
return import.meta.env.DEV ? (
<ReactQueryDevtoolsPanel
queryClient={queryClient}
style=
onClose={() => console.log('Devtools hidden')}
/>
) : null;
}
Такой компонент можно встроить в сторибуки, дизайн-системы или вынести в отдельное окно Electron.
Last Updated).staleTime, gcTime, select). Удобно искать компоненты, не отписавшиеся при размонтировании.refetch, invalidate, reset, remove, setData. Кнопки позволяют симулировать фоновые события без изменения кода.clear) или принудительно прервать.Focus refetching, Reconnect refetching, Structural sharing.document.body.dataset.theme = 'dark', чтобы проверить, не конфликтуют ли стили проекта.toggleButtonProps={{ onKeyDown: e => e.stopPropagation() }}.panelProps={{ style: { fontSize: '14px', padding: '1rem' } }} и закрепите кнопку сверху.showStale. Если запрос мгновенно становится stale, значит staleTime = 0 и каждый фокус окна будет вызывать refetch.[object Object], значит передаётся немемуизированный объект. В Devtools это легко увидеть, включив Show query keys.options. Ищите параметры по умолчанию, которые не переопределены (например, refetchOnWindowFocus: true).observerAdded, observerRemoved, queryUpdated, queryGC. Если observerAdded срабатывает чаще, чем ожидалось, значит компоненты пересоздаются.queryClient.getQueryCache().find({ queryKey })?.observers.length в runtime-логах, чтобы сверять с Devtools.const unsubscribe = queryClient.getQueryCache().subscribe(event => {
if (event?.type === 'queryObserverResultUpdated' && event.query.state.fetchStatus === 'fetching') {
performance.mark(`rq:${event.query.queryHash}:fetching`);
}
if (event?.type === 'queryRemoved') {
console.debug('[RQ] GC', event.query.queryKey);
}
});
queryClient.getMutationCache().subscribe(event => {
if (event?.type === 'mutationAdded') {
console.info('[RQ] Mutation enqueued', event.mutation.options.mutationKey);
}
});
unsubscribe() при размонтировании инструментов (например, в отдельном хелпере useReactQueryLogger()).observer-апдейтов в Devtools. Если React показывает больше, проблема не в Query.select: если он возвращает новый объект без мемоизации, Devtools покажет обновление данных, даже если query.state.data не менялась.notifyOnChangeProps: ['data', 'error'], чтобы убедиться, что Query не гоняет лишние уведомления.React.unstable_Tracing или startTransition (React 18) для тяжелых ререндеров, чтобы Devtools не мешали профайлу.staleTime > 0 + refetchOnWindowFocus: false хорошо подходит для страниц, куда пользователь часто возвращается по кнопке «Назад».gcTime: Infinity удобно для справочников, но следите за персистерами: devtools покажут общий объём через Cache size.keepPreviousData: true и смотрите в Devtools, как меняется status (будет pending с isPreviousData: true).dataUpdatedAt до и после dehydrate() — Devtools подсветит, если данные сбрасываются на клиенте.failureCount и meta. Если там пусто, значит не передали meta в useMutation.mutation.reset() очищает ошибку и статус, но данные оптимистичного обновления вам нужно откатить вручную через onError.mutate внутри useEffect без массива зависимостей; Devtools покажет очередь из одинаковых мутаций.suspense: true + useDeferredValue, чтобы React батчил обновления гигантских таблиц после получения данных.queryClient.setQueryData с structuralSharing: (prev, next) => fastDeepEqual(prev, next) или false, если шаринг дороже пересоздания.@tanstack/react-virtual) и смотрите, как Observers уменьшаются до одного списка вместо сотен строковых компонентов.Refetch во время скролла, чтобы проверить, не проседает ли FPS при одновременном обновлении и виртуализации.addDecorator(Story => (
<QueryClientProvider client={queryClient}>
<Story />
<ReactQueryDevtools initialIsOpen={true} position="top" />
</QueryClientProvider>
));
ReactQueryDevtoolsPanel, чтобы дизайнеры могли сами смотреть на данные.storybook-addon-performance, чтобы сравнивать FPS при разных опциях staleTime.performance.mark) в subscribe, чтобы на таймлайне видеть моменты fetch.react-query в логах (console.time('rq fetch')) и сравнивайте с сетью.refetchOnReconnect).Focus Refetching, а затем настройте refetchOnWindowFocus: false.dataUpdatedAt и staleTime, убедитесь, что нет фонового retry.Infinite Query, проверьте pageParam в ключе и hasNextPage.onMutate / context, убедитесь, что queryClient.setQueryData не перезаписывает весь массив.failureCount → проверяем retry и таймауты.Observers > ожидаемого → компонент пересоздаёт queryKey.Status: fetching без Network-запроса → запрос отменён и перезапущен (проверьте abortController).GC срабатывает рано → слишком маленький gcTime, увеличьте или держите placeholderData.if (import.meta.env.DEV) {
queryClient.setDefaultOptions({
queries: {
retryDelay: attempt => Math.min(1000 * attempt, 5000),
networkMode: 'offlineFirst',
},
});
}
Совместите с Chrome Network Throttling и наблюдайте в Devtools, как растёт failureCount. Это помогает подобрать реалистичные retry.