Query invalidation по событиям

Механизм query invalidation используется для принудительного помечания кешированных запросов как устаревших. После инвалидирования TanStack Query инициирует повторную загрузку данных или откладывает её до следующего момента обращения к запросу — в зависимости от конфигурации.

Инвалидация является центральным механизмом синхронизации клиентского кеша с серверным состоянием. Вместо ручного обновления всех структур данных библиотека позволяет пометить определённые запросы как неактуальные и автоматически перезапросить данные.

Базовый пример:

queryClient.invalidateQueries({
    queryKey: ['posts']
});

После вызова:

['posts']

все запросы с таким ключом переходят в состояние stale.


Проблема устаревших данных

Серверное состояние постоянно изменяется:

  • создание сущностей;
  • удаление записей;
  • изменение профилей;
  • обновление комментариев;
  • получение WebSocket-событий;
  • синхронизация между вкладками;
  • фоновые процессы;
  • cron-задачи;
  • действия других пользователей.

Без invalidation UI быстро начинает отображать устаревшие данные.

Пример:

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
});

Если пользователь создал нового пользователя через mutation:

await createUser(data);

список users останется старым, пока не произойдёт refetch.

Именно для этого используется invalidation.


Связь invalidation и событий

В TanStack Query invalidation почти всегда инициируется событием.

Типичные источники событий:

Событие Пример
Mutation Создание записи
WebSocket Новый комментарий
SSE Серверное уведомление
Таймер Автообновление
Focus event Возврат на вкладку
Reconnect Восстановление сети
Broadcast Изменение в другой вкладке
Custom event Пользовательское действие
Route change Переход между страницами

Invalidation после mutation

Наиболее распространённый сценарий.

Базовый пример

const queryClient = useQueryClient();

const mutation = useMutation({
    mutationFn: createPost,

    onSuccess: () => {
        queryClient.invalidateQueries({
            queryKey: ['posts']
        });
    }
});

После успешного создания записи:

  1. запрос posts помечается stale;
  2. выполняется повторная загрузка;
  3. UI получает актуальный список.

Почему invalidateQueries лучше ручного refetch

Многие разработчики делают так:

const { refetch } = useQuery(...);

await mutation.mutateAsync(data);

refetch();

Подход имеет недостатки:

  • требуется доступ к refetch;
  • сложнее синхронизация;
  • появляется связность компонентов;
  • невозможно централизованно управлять кешем;
  • возникают дублирующиеся refetch.

invalidateQueries работает на уровне глобального кеша.


Частичная инвалидация по queryKey

TanStack Query поддерживает частичное совпадение ключей.

Пример структуры:

['posts']
['posts', 1]
['posts', 2]
['posts', 'comments']

Инвалидация:

queryClient.invalidateQueries({
    queryKey: ['posts']
});

затронет все перечисленные запросы.


Точная инвалидация

Для ограничения области invalidation используется exact.

queryClient.invalidateQueries({
    queryKey: ['posts'],
    exact: true
});

Теперь будет инвалидирован только:

['posts']

но не:

['posts', 1]

Invalidation после обновления сущности

Пример обновления пользователя

const mutation = useMutation({
    mutationFn: updateUser,

    onSuccess: (updatedUser) => {
        queryClient.invalidateQueries({
            queryKey: ['users']
        });

        queryClient.invalidateQueries({
            queryKey: ['user', updatedUser.id]
        });
    }
});

Инвалидируются:

  • список пользователей;
  • конкретный пользователь.

Invalidation по сложным событиям

Иногда одно действие влияет на несколько областей приложения.

Пример интернет-магазина:

const mutation = useMutation({
    mutationFn: createOrder,

    onSuccess: () => {
        queryClient.invalidateQueries({
            queryKey: ['cart']
        });

        queryClient.invalidateQueries({
            queryKey: ['orders']
        });

        queryClient.invalidateQueries({
            queryKey: ['inventory']
        });

        queryClient.invalidateQueries({
            queryKey: ['profile']
        });
    }
});

Создание заказа влияет сразу на:

  • корзину;
  • остатки товаров;
  • историю заказов;
  • бонусы пользователя.

Централизация invalidation

При большом проекте invalidation начинает дублироваться.

Плохой пример:

onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['posts'] });
    queryClient.invalidateQueries({ queryKey: ['feed'] });
    queryClient.invalidateQueries({ queryKey: ['stats'] });
}

Лучше выносить в отдельные функции.


Слой invalidation-сервисов

export const invalidatePostQueries = async (queryClient) => {
    await Promise.all([
        queryClient.invalidateQueries({
            queryKey: ['posts']
        }),

        queryClient.invalidateQueries({
            queryKey: ['feed']
        }),

        queryClient.invalidateQueries({
            queryKey: ['stats']
        })
    ]);
};

Использование:

onSuccess: async () => {
    await invalidatePostQueries(queryClient);
}

Автоматизация invalidation через mutation factory

Унифицированный mutation-хелпер

export const createMutation = ({
    mutationFn,
    invalidate = []
}) => {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn,

        onSuccess: async () => {
            await Promise.all(
                invalidate.map((key) =>
                    queryClient.invalidateQueries({
                        queryKey: key
                    })
                )
            );
        }
    });
};

Использование:

const mutation = createMutation({
    mutationFn: createPost,

    invalidate: [
        ['posts'],
        ['feed']
    ]
});

События WebSocket и invalidation

Инвалидация по push-событиям

Сервер может отправлять уведомления:

{
    "type": "NEW_MESSAGE",
    "chatId": 15
}

Обработка:

socket.onmess age = (event) => {
    const data = JSON.parse(event.data);

    if (data.type === 'NEW_MESSAGE') {
        queryClient.invalidateQueries({
            queryKey: ['chat', data.chatId]
        });
    }
};

Invalidation в realtime-приложениях

Realtime-системы активно используют invalidation:

  • чаты;
  • биржи;
  • совместные редакторы;
  • панели мониторинга;
  • CRM;
  • трекеры задач.

Главная идея:

  • сервер сообщает о событии;
  • клиент инвалидирует кеш;
  • TanStack Query выполняет refetch.

Invalidation против setQueryData

Существует два подхода обновления кеша.

Через invalidateQueries

queryClient.invalidateQueries({
    queryKey: ['posts']
});

Через setQueryData

queryClient.setQueryData(
    ['posts'],
    (old) => [...old, newPost]
);

Когда лучше invalidateQueries

invalidateQueries предпочтителен:

  • при сложной серверной логике;
  • при наличии вычисляемых полей;
  • при пагинации;
  • при серверной сортировке;
  • при фильтрации;
  • при связанных сущностях;
  • при неизвестном финальном состоянии.

Когда лучше setQueryData

setQueryData полезен:

  • для мгновенного UI;
  • для optimistic updates;
  • при небольших локальных изменениях;
  • если итоговое состояние известно заранее.

Комбинированный подход

Часто используется одновременно:

onSuccess: (newPost) => {
    queryClient.setQueryData(
        ['post', newPost.id],
        newPost
    );

    queryClient.invalidateQueries({
        queryKey: ['posts']
    });
}

Инвалидация по focus-событиям

TanStack Query умеет автоматически рефетчить данные при возвращении на вкладку.

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            refetchOnWindowFocus: true
        }
    }
});

Механизм основан на invalidation stale-запросов.


Инвалидация при восстановлении сети

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            refetchOnReconnect: true
        }
    }
});

После reconnect запросы обновляются автоматически.


Пользовательские события invalidation

Event Bus

eventBus.on('USER_UPDATED', (userId) => {
    queryClient.invalidateQueries({
        queryKey: ['user', userId]
    });
});

DOM-события

window.addEventListener('profile-updated', () => {
    queryClient.invalidateQueries({
        queryKey: ['profile']
    });
});

Invalidation по таймеру

Периодическая синхронизация

setInterval(() => {
    queryClient.invalidateQueries({
        queryKey: ['notifications']
    });
}, 30000);

Разница между refetch и invalidation

refetch

Немедленно выполняет запрос.

refetch();

invalidateQueries

Помечает запрос устаревшим.

queryClient.invalidateQueries(...)

Refetch может быть выполнен позже.


Фильтрация invalidateQueries

TanStack Query поддерживает гибкую фильтрацию.

По predicate

queryClient.invalidateQueries({
    predicate: (query) => {
        return query.queryKey[0] === 'posts';
    }
});

Инвалидация активных запросов

queryClient.invalidateQueries({
    queryKey: ['posts'],
    refetchType: 'active'
});

Варианты:

Значение Описание
active Только активные
inactive Только неактивные
all Все

Массовая инвалидация

Иногда требуется обновить весь кеш.

queryClient.invalidateQueries();

Но такой подход:

  • создаёт лишний трафик;
  • ухудшает производительность;
  • вызывает каскадные refetch;
  • увеличивает нагрузку на API.

Архитектура query keys и invalidation

Качество invalidation напрямую зависит от структуры query keys.

Плохая структура:

['data']

Невозможно гибко инвалидировать данные.

Хорошая структура:

['users']
['users', userId]
['users', userId, 'posts']
['posts']
['posts', postId]

Событийная модель invalidation

Крупные приложения часто строят вокруг событийной архитектуры.

Пример

eventBus.emit('POST_CREATED', post);

Отдельный слой подписывается:

eventBus.on('POST_CREATED', () => {
    queryClient.invalidateQueries({
        queryKey: ['posts']
    });

    queryClient.invalidateQueries({
        queryKey: ['feed']
    });
});

Mutation ничего не знает о кешах.

Это уменьшает связность системы.


Invalidation в микрофронтендах

При микрофронтенд-архитектуре события могут приходить из разных приложений.

window.dispatchEvent(
    new CustomEvent('cart:updated')
);

Другой frontend:

window.addEventListener('cart:updated', () => {
    queryClient.invalidateQueries({
        queryKey: ['cart']
    });
});

Broadcast invalidation между вкладками

Для синхронизации вкладок используется BroadcastChannel.

const channel = new BroadcastChannel('app');

channel.onmess age = (event) => {
    if (event.data.type === 'INVALIDATE_POSTS') {
        queryClient.invalidateQueries({
            queryKey: ['posts']
        });
    }
};

Invalidation и staleTime

staleTime влияет на поведение invalidation.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    staleTime: 1000 * 60 * 5
});

Даже если staleTime ещё не истёк:

queryClient.invalidateQueries({
    queryKey: ['posts']
});

запрос немедленно становится stale.


Invalidation и cacheTime

cacheTime определяет:

  • сколько данные живут в кеше;
  • когда удаляются неактивные запросы.

Но invalidation не удаляет кеш.

Она только помечает его устаревшим.


Invalidation и optimistic updates

Типичный сценарий

const mutation = useMutation({
    mutationFn: updatePost,

    onMutate: async (updatedPost) => {
        await queryClient.cancelQueries({
            queryKey: ['posts']
        });

        const previous =
            queryClient.getQueryData(['posts']);

        queryClient.setQueryData(
            ['posts'],
            (old) =>
                old.map((post) =>
                    post.id === updatedPost.id
                        ? updatedPost
                        : post
                )
        );

        return { previous };
    },

    onError: (error, variables, context) => {
        queryClient.setQueryData(
            ['posts'],
            context.previous
        );
    },

    onSettled: () => {
        queryClient.invalidateQueries({
            queryKey: ['posts']
        });
    }
});

Почему invalidate вызывается в onSettled

onSettled выполняется:

  • после успеха;
  • после ошибки.

Это гарантирует синхронизацию с сервером независимо от результата optimistic update.


Invalidation и бесконечные запросы

Для useInfiniteQuery invalidation работает аналогично.

queryClient.invalidateQueries({
    queryKey: ['feed']
});

Все страницы будут считаться stale.


Invalidation и SSR

При SSR invalidation используется осторожно.

Проблемы:

  • лишние refetch после hydration;
  • дублирование запросов;
  • рассинхронизация клиента и сервера.

Инвалидация при logout

Типичный пример:

const logout = async () => {
    await api.logout();

    queryClient.clear();
};

Иногда лучше:

queryClient.invalidateQueries();

Но clear() полностью очищает кеш и обычно безопаснее при смене пользователя.


Devtools и отладка invalidation

TanStack Query Devtools позволяют видеть:

  • какие запросы stale;
  • какие refetch выполняются;
  • какие query keys инвалидируются;
  • время обновления;
  • активные observers.

Это критически важно для диагностики сложных invalidation-сценариев.


Типичные ошибки

Чрезмерная инвалидация

queryClient.invalidateQueries();

после любой mutation.

Результат:

  • лавина запросов;
  • лишняя нагрузка;
  • деградация UX.

Слишком общие query keys

['data']

Невозможно управлять отдельными частями кеша.


Отсутствие invalidation

Mutation завершилась успешно, но UI остался старым.


Дублирование refetch

await mutation.mutateAsync();

refetch();

queryClient.invalidateQueries(...);

Происходит двойной запрос.


Использование setQueryData без серверной синхронизации

Локальный кеш обновлён, но сервер вернул другое состояние.


Рекомендации по архитектуре

Использовать событийный подход

Mutation должна сообщать о событии, а не управлять кешем напрямую.


Делать query keys предсказуемыми

Иерархическая структура:

['users']
['users', id]
['users', id, 'posts']

значительно упрощает invalidation.


Минимизировать глобальную инвалидацию

Лучше инвалидировать:

['posts']

чем весь кеш.


Комбинировать optimistic updates и invalidation

Это даёт:

  • мгновенный UI;
  • корректную синхронизацию;
  • согласованность с сервером.

Выносить invalidation в отдельный слой

Крупные приложения выигрывают от централизованного управления событиями и кешем.