Зависимые запросы — это механизм, при котором выполнение одного HTTP-запроса зависит от результата другого. В RTK Query подобная схема используется очень часто:
RTK Query предоставляет несколько способов организации зависимых запросов:
skipskipTokenqueryFnНаиболее распространённый сценарий:
Пример:
const { data: user } = useGetUserQuery(userId);
const { data: posts } = useGetPostsByUserQuery(user?.id, {
skip: !user?.id
});
Логика работы:
useGetUserQuery загружает пользователя;user.id отсутствует — второй запрос не
выполняется;user.id автоматически запускается
useGetPostsByUserQuery.skip — основной механизм условного отключения
запроса.
useSomeQuery(arg, {
skip: boolean
});
Если skip === true:
uninitialized.export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getProfile: builder.query({
query: () => '/profile'
}),
getOrders: builder.query({
query: (userId) => `/users/${userId}/orders`
})
})
});
Использование:
function OrdersPage() {
const { data: profile } = useGetProfileQuery();
const {
data: orders,
isLoading
} = useGetOrdersQuery(profile?.id, {
skip: !profile?.id
});
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
{orders?.map(order => (
<div key={order.id}>
{order.title}
</div>
))}
</div>
);
}
При первом рендере:
profile = undefined
Следовательно:
skip: true
Запрос getOrders не стартует.
После завершения getProfile:
profile = {
id: 15
}
Теперь:
skip: false
RTK Query автоматически выполняет:
GET /users/15/orders
Если запрос пропущен:
const result = useGetOrdersQuery(userId, {
skip: true
});
Состояние:
{
isUninitialized: true,
isLoading: false,
isFetching: false,
data: undefined
}
Это важно при построении UI.
Пример:
if (result.isUninitialized) {
return <div>Ожидание userId</div>;
}
Частая ошибка:
useGetUserQuery(undefined);
Некоторые API могут интерпретировать это как:
/users/undefined
Правильный подход:
useGetUserQuery(userId, {
skip: !userId
});
RTK Query предоставляет специальный объект:
skipToken
Он полностью отключает запрос без использования
skip.
import { skipToken } from '@reduxjs/toolkit/query';
const { data } = useGetUserQuery(
userId ?? skipToken
);
Если userId === null или undefined:
skipToken
запрос не выполняется.
Вместо:
useGetUserQuery(userId, {
skip: !userId
});
используется:
useGetUserQuery(userId ?? skipToken);
Особенно полезно в TypeScript.
Без skipToken:
useGetUserQuery(userId as number)
Со skipToken:
useGetUserQuery(userId ?? skipToken)
Типы становятся безопаснее.
RTK Query позволяет строить длинные цепочки.
const { data: session } = useGetSessionQuery();
const { data: profile } = useGetProfileQuery(
session?.userId,
{
skip: !session?.userId
}
);
const { data: permissions } = useGetPermissionsQuery(
profile?.roleId,
{
skip: !profile?.roleId
}
);
Последовательность:
Session -> Profile -> Permissions
Слишком длинные цепочки создают:
Waterfall — последовательные сетевые запросы.
Плохой пример:
1 секунда -> user
1 секунда -> posts
1 секунда -> comments
Итог:
3 секунды
Хотя запросы могли бы выполняться параллельно.
Зависимость оправдана, если:
Нежелательно:
user -> posts -> comments -> reactions -> likes
Лучше:
RTK Query поддерживает ручной запуск запросов.
Используется hook:
useLazyQuery
const [fetchUser, result] = useLazyGetUserQuery();
Запуск:
await fetchUser(15);
function Page() {
const [fetchUser] = useLazyGetUserQuery();
const [fetchPosts] = useLazyGetPostsQuery();
const load = async () => {
const userResult = await fetchUser(1);
if (userResult.data) {
await fetchPosts(userResult.data.id);
}
};
useEffect(() => {
load();
}, []);
return <div>Page</div>;
}
Можно:
Например:
login
↓
получение токена
↓
получение профиля
↓
получение настроек
↓
redirect
const load = async () => {
const userResult = await fetchUser(1);
if (userResult.error) {
return;
}
const postsResult = await fetchPosts(
userResult.data.id
);
if (postsResult.error) {
console.error(postsResult.error);
}
};
RTK Query поддерживает:
.unwrap()
Это позволяет использовать try/catch.
const load = async () => {
try {
const user = await fetchUser(1).unwrap();
const posts = await fetchPosts(user.id).unwrap();
console.log(posts);
} catch (error) {
console.error(error);
}
};
queryFn позволяет объединить несколько запросов в одном
endpoint.
getUserWithPosts: builder.query({
async queryFn(userId, _api, _extraOptions, baseQuery) {
const userResult = await baseQuery(
`/users/${userId}`
);
if (userResult.error) {
return {
error: userResult.error
};
}
const postsResult = await baseQuery(
`/users/${userId}/posts`
);
if (postsResult.error) {
return {
error: postsResult.error
};
}
return {
data: {
user: userResult.data,
posts: postsResult.data
}
};
}
})
Компонент получает:
const { data } = useGetUserWithPostsQuery(1);
вместо:
user query
↓
posts query
↓
conditions
↓
loading states
Вся логика ошибок хранится внутри endpoint.
isLoading
isError
data
относятся сразу ко всей цепочке.
Можно комбинировать последовательные и параллельные операции.
async queryFn(userId, _api, _extraOptions, baseQuery) {
const userResult = await baseQuery(
`/users/${userId}`
);
if (userResult.error) {
return {
error: userResult.error
};
}
const [postsResult, commentsResult] =
await Promise.all([
baseQuery(`/users/${userId}/posts`),
baseQuery(`/users/${userId}/comments`)
]);
return {
data: {
user: userResult.data,
posts: postsResult.data,
comments: commentsResult.data
}
};
}
Мутации также могут зависеть друг от друга.
const [login] = useLoginMutation();
const [loadProfile] = useLazyGetProfileQuery();
const handleLogin = async () => {
try {
const auth = await login({
email,
password
}).unwrap();
await loadProfile(auth.userId).unwrap();
} catch (error) {
console.error(error);
}
};
Типичная схема:
login
↓
save token
↓
get profile
↓
get permissions
↓
open application
RTK Query хорошо подходит для подобных flow.
Иногда второй запрос зависит не только от наличия данных, но и от их содержимого.
const { data: user } = useGetUserQuery(id);
const { data: premium } =
useGetPremiumDataQuery(user?.id, {
skip: user?.role !== 'premium'
});
Запрос выполняется только для premium-пользователей.
Polling тоже можно комбинировать с зависимостями.
const { data: task } = useGetTaskQuery(taskId);
const { data: progress } =
useGetProgressQuery(task?.id, {
skip: !task?.id,
pollingInterval: 3000
});
RTK Query автоматически отслеживает изменения аргументов.
const { data: user } = useGetUserQuery(id);
const { data: posts } =
useGetPostsQuery(user?.id, {
skip: !user?.id
});
Если user.id изменится:
старый cache key удаляется
↓
создаётся новый запрос
↓
данные обновляются
Каждый endpoint кэшируется отдельно.
useGetUserQuery(1)
useGetPostsQuery(1)
создают два независимых cache entry.
Изменение пользователя не инвалидирует posts автоматически.
Для синхронизации используются теги.
getPosts: builder.query({
query: (userId) => `/users/${userId}/posts`,
providesTags: ['Posts']
}),
updatePost: builder.mutation({
query: (post) => ({
url: `/posts/${post.id}`,
method: 'PUT',
body: post
}),
invalidatesTags: ['Posts']
})
При server-side rendering важно:
На сервере:
skip: true
На клиенте:
skip: false
Это может привести к hydration mismatch.
RTK Query поддерживает prefetch.
dispatch(
api.util.prefetch(
'getPosts',
userId,
{
force: true
}
)
);
Пример:
hover user
↓
prefetch profile
↓
open page
↓
profile already cached
↓
dependent queries start instantly
Позволяет уменьшать ререндеры.
const { userId } = useGetSessionQuery(undefined, {
selectFromResult: ({ data }) => ({
userId: data?.userId
})
});
Далее:
const { data: profile } =
useGetProfileQuery(userId, {
skip: !userId
});
Лучше:
user
↓
parallel:
posts
comments
settings
чем:
user
↓
posts
↓
comments
↓
settings
Иногда эффективнее:
GET /dashboard
вместо:
/users
/posts
/comments
/stats
Сложные цепочки лучше скрывать внутри API-слоя.
Плохо:
useEffect(() => {
fetchUser(id);
}, [id]);
Если можно использовать обычный query hook.
Плохо:
A -> B -> C -> D -> E
Плохо:
if (!user) return null;
const query = useGetPostsQuery(user.id, {
skip: !user
});
Достаточно одного механизма.
Наиболее стабильная схема:
Только простые зависимости:
skip
skipToken
Сложная orchestration:
queryFn
Асинхронные flow:
lazy queries
mutations
unwrap
const [login] = useLoginMutation();
const [loadProfile] =
useLazyGetProfileQuery();
const [loadPermissions] =
useLazyGetPermissionsQuery();
const handleAuth = async () => {
try {
const auth = await login({
email,
password
}).unwrap();
const profile = await loadProfile(
auth.userId
).unwrap();
const permissions =
await loadPermissions(
profile.roleId
).unwrap();
console.log(
auth,
profile,
permissions
);
} catch (error) {
console.error(error);
}
};
Подходит для:
Подходит для:
Подходят для:
Подходит для: