Можно ли передать в React-хук другой хук (параметром/пропсом) и там им пользоваться?
Вопрос в том, что я хочу сделать универсальный хук который будет либо получать данные для сущности, используя хук из RTKQuery (например хук useLazyGetUserQuery), либо получать начальные данные сущности при добавлении новой сущности, также используя хук из RTKQuery (например хук useLazyGetDefaultUserQuery).
Сущностей много и не хочется делать для каждой сущности свой отдельный хук. И все эти хуки будут различаться только хуками запросов к серверу. Стоит ли передавать хук в другой хук (параметром/пропсом)? Или это может как-то поломать React? И также еще немного дополнительный вопрос: как такой универсальный хук можно типизировать? (касаемо типизации хуков RTKQuery. Нужен такой тип данных, который включает в себя RTKQuery-Lazy-хуки)
Вот пример хука:
// Типизация плохая, но рабочая. Думаю есть более хороший подход
type QueryTrigger<T> = (arg: T) => { unwrap: () => Promise<any> };
type LazyQueryResult<TData> = { data?: TData; isFetching: boolean; [key: string]: unknown };
type LazyQueryHook<TData, TArg> = () => [QueryTrigger<TArg>, LazyQueryResult<TData>, unknown];
interface UseEntityFormParams<T extends object, ID> {
useLazyGetEntityQuery: LazyQueryHook<T, ID>;
useLazyGetDefaultEntityQuery: LazyQueryHook<T, void>;
ref: React.ForwardedRef<FormHandles>;
entityId?: ID;
propEntityData?: T;
otherFetching: boolean;
}
export const useEntityForm = <T extends object, ID>({
ref,
entityId,
propEntityData,
otherFetching,
useLazyGetEntityQuery,
useLazyGetDefaultEntityQuery,
}: UseEntityFormParams<T, ID>) => {
// Использование хуков, переданных через параметры/пропсы
const [triggerGetEntity, { data: entityDataFromDb, isFetching: isFetchingEntity }] =
useLazyGetEntityQuery();
const [triggerGetDefaultEntity, { isFetching: isFetchingDefaultEntity }] =
useLazyGetDefaultEntityQuery();
...
}
Вот пример вызова:
import { useLazyGetDefaultUserQuery, useLazyGetUserQuery } from '../../api/userApi'; <-- это хуки из RTKQuery
const {
form,
entityData: userData,
isFetchingComplete,
} = useEntityForm<User, number>({
useLazyGetEntityQuery: useLazyGetUserQuery,
useLazyGetDefaultEntityQuery: useLazyGetDefaultUserQuery,
ref: ref,
entityId: userId,
propEntityData: propUserData,
otherFetching: false,
});
RTKQuery-хуки:
import { myApi } from '@/shared/api';
import { User } from '@/entities/security-module/user';
export type UserId = User['id'];
export const userApi = myApi.injectEndpoints({
endpoints: (builder) => ({
getDefaultUser: builder.query<User, void>({
query: () => ({
url: '/Users/GetDefault',
method: 'GET',
}),
}),
getUser: builder.query<User, UserId>({
query: (userId) => ({
url: '/Users/GetById',
params: { id: userId },
}),
}),
...
}),
overrideExisting: true,
});
export const {
useGetDefaultUserQuery,
useLazyGetDefaultUserQuery,
useGetUserQuery,
useLazyGetUserQuery,
} = userApi;
Ответы (1 шт):
Можно. Но неплохо бы гарантировать, что в этот параметр залетает один и тот же хук.
Если в твоем примере вызова сделать так: useLazyGetEntityQuery: condition ? useHook1 : useHook2, и если при этом внутри useHook1 и useHook2 разные порядки вызова системных хуков (что весьма вероятно), то произойдет ербалайка. И главное, этот момент не отслеживается линтером.
Как вариант, внутри useEntityForm можно сделать проверку на постоянство хука:
const useSavedLazyGetEntityQuery = useRef(useLazyGetEntityQuery).current;
if (useSavedLazyGetEntityQuery !== useLazyGetEntityQuery) {
throw new Error('Изменился хук useLazyGetEntityQuery');
}
Это поможет сразу отчетливо увидеть проблему, увы, только в рантайме, но тем не менее.
Еще способ - сделать не хук useEntityForm, а "фабрику хука" для него, которую использовать примерно так:
const useEntityFormWithUserQuery = makeUseEntityForm({
useLazyGetUserQuery,
useLazyGetDefaultUserQuery
})
Т.е. сразу прибиваешь гвоздями все хуки, и далее в созданный таким образом useEntityFormWithUserQuery не надо передавать твои два хука, они уже хранятся в замыкании
Набросок фабрики:
interface UseFactoryParams<T, ID> {
useLazyGetEntityQuery: LazyQueryHook<T, ID>;
useLazyGetDefaultEntityQuery: LazyQueryHook<T, void>;
}
interface UseEntityFormParams<T, ID> {
ref: React.ForwardedRef<FormHandles>;
entityId?: ID;
propEntityData?: T;
otherFetching: boolean;
}
export const makeUseEntityForm = <T extends object, ID>({
useLazyGetEntityQuery,
useLazyGetDefaultEntityQuery
}: UseFactoryParams<T, ID>) => ({
ref,
entityId,
propEntityData,
otherFetching,
}: UseEntityFormParams<T, ID>) => {
// Использование хуков, переданных через параметры/пропсы
const [triggerGetEntity, { data: entityDataFromDb, isFetching: isFetchingEntity }] =
useLazyGetEntityQuery();
const [triggerGetDefaultEntity, { isFetching: isFetchingDefaultEntity }] =
useLazyGetDefaultEntityQuery();
...
};