Динамические Breadcrumbs на React, React Router и Apollo GraphQL
Хлебные крошки — важнейшая часть навигации приложения. В классическом исполнении они отражают текущее положение пользователя в иерархии. А отображение названия карточки товара, статьи или любой другой сущности — это уже, как правило, задача компонента отвечающего за отображение самой сущности. Однако, все может оказаться не так просто. По каким-либо причинам дизайнер, заказчик или другая неведомая сила будет категорично настаивать, чтобы название отображалось именно в хлебных крошках.
Как бы то ни было, задача есть и ее нужно закрыть. Поэтому я и расскажу, как я с ней справился, в надежде получить одобрение или более элегантное решение) Погнали!
Я решал эту задачу в React-приложении, в котором используется React Router 6 версии, дизайн-система Ant, а также Apollo GraphQL и TanStack Query для обращения к API.
Для примера приведу простенькую маршрутизацию по коллекциям книг и фигурок, но этого будет достаточно, чтобы погрузиться в контекст.
Сначала схема:
root/
collections/
books/
:id
figurines/
:id
Теперь код:
export const AppRouter = () => {
const router = createBrowserRouter(
createRoutesFromElements(
}
errorElement={ }
>
}
errorElement={ }
handle={{
crumb: () => {'Коллекции'},
}}
>
}
errorElement={ }
handle={{
crumb: () => {'Фигурки'},
}}
>
}
errorElement={ }
handle={{
crumb: (id: string) => (
response?.data.name ?? 'Имя книги отсутствует'}
to={`${ROUTES_PATHS.books}/${id}`}
useApolloLazyQuery={useGetBookByIdLazyQuery}
payload={id}
/>
),
}}
/>
}
/>
}
errorElement={ }
handle={{
crumb: () => {'Фигурки'},
}}
>
}
errorElement={ }
handle={{
crumb: (id: string) => (
response?.data.name ?? 'Имя фигурки отсутствует'}
to={`${ROUTES_PATHS.figurines}/${id}`}
useRestLazyQuery={useGetFigureByIdLazyQuery}
payload={id}
/>
),
}}
/>
}
/>
}
/>
}
/>
,
),
);
return ;
};
Вся магия происходит в поле crumb
объекта handle
. В документации React Router`а я нашел, что через свойство handle
можно создавать методы, которые будут принимать заранее загруженные данные через свойство loader
компонента Route
, но также можно использовать свойство handle
, как некое «хранилище» для конкретного маршрута, которым можно воспользоваться через useMatches.
Давайте посмотрим на примере компонента Breadcrumbs
:
export const Breadcrumbs: React.FC = () => {
const matches = useMatches();
const crumbs = React.useMemo(
() =>
matches
.filter((match) => Boolean((match.handle as { crumb?: CrumbType })?.crumb))
.map((match) => (match.handle as { crumb: CrumbType })?.crumb(match.params.id)),
[matches],
);
const extraBreadcrumbItems = React.useMemo(
() =>
crumbs.map((crumb) => ({
key: uuidv4(),
title: crumb,
})),
[crumbs],
);
const breadcrumbItems = React.useMemo(
() => [
{
key: 'home',
title: (
{'Главная'}
),
},
...extraBreadcrumbItems,
],
[extraBreadcrumbItems],
);
return (
<>
>
);
};
Явный минус, что не получится типизировать подобное «хранилище». Поэтому нужно позаботиться о проверках, дабы приложение случайно не упало, если вдруг забудем передать «крошку». Также если кому интересно, что из себя представляет компонент Breadcrumb
, то это можно посмотреть в документации antd.
Осталось показать, что из себя представляют ApolloDataLink
и RestDataLink
— компоненты, которые делают сам запрос для получения названия «крошки».
ApolloDataLink
:
interface IApolloDataLink> {
getLinkText: (data: TData | undefined) => string;
to: string;
useLazyQuery: () => Apollo.LazyQueryResultTuple;
variables: TVariables;
}
function ApolloDataLink>(props: IApolloDataLinkLink) {
const { getLinkText, useLazyQuery, to, variables } = props;
const [getData, { data }] = useLazyQuery();
React.useEffect(() => {
getData({ variables });
}, [variables]); // eslint-disable-line react-hooks/exhaustive-deps
return {getLinkText(data)};
}
RestDataLink
:
interface IRestDataLink {
getLinkText: (data: TData | undefined) => string;
to: string;
useRestLazyQuery: (
payloadAsKey: TPayload,
) => [call: (payload: TPayload) => void, query: UseQueryResult];
payload: TPayload;
}
function RestDataLink(props: IRestDataLink) {
const { getLinkText, useRestLazyQuery, to, payload } = props;
const [getData, { data }] = useRestLazyQuery(payload);
React.useEffect(() => {
getData(payload);
}, [payload]); // eslint-disable-line react-hooks/exhaustive-deps
return {getLinkText(data)};
}
Не спрашивайте, почему я использую сразу и Apollo и TanStack, так как это уже вопрос о переобувании авторов различных backend-сервисов в приложении, которое и породило идею этой статьи.
Но главное, что и Apollo и TanStack имеют кэш. Поэтому при отображении компонента конкретной сущности данные берутся уже из кэша.
Вроде вышло немного, но в свое время я посидел часа три над этой задачей. Надеюсь, что эта статья будет полезной. Счастливого кодинга!
P.S. В TanStack нет ленивых запросов подобных Apollo, но легко сделать, что-то подобное, прочитав вот этот раздел документации.