Pull to refresh

Динамические Breadcrumbs на React, React Router и Apollo GraphQL

Level of difficultyEasy
Reading time4 min
Views4K

Хлебные крошки - важнейшая часть навигации приложения. В классическом исполнении они отражают текущее положение пользователя в иерархии. А отображение названия карточки товара, статьи или любой другой сущности - это уже, как правило, задача компонента отвечающего за отображение самой сущности. Однако, все может оказаться не так просто. По каким-либо причинам дизайнер, заказчик или другая неведомая сила будет категорично настаивать, чтобы название отображалось именно в хлебных крошках.

Как бы то ни было, задача есть и ее нужно закрыть. Поэтому я и расскажу, как я с ней справился, в надежде получить одобрение или более элегантное решение) Погнали!

Я решал эту задачу в React-приложении, в котором используется React Router 6 версии, дизайн-система Ant,а также Apollo GraphQL и TanStack Query для обращения к API.

Для примера приведу простенькую маршрутизацию по коллекциям книг и фигурок, но этого будет достаточно, чтобы погрузиться в контекст.

Сначала схема:

root/
  collections/
    books/
      :id
    figurines/
      :id

Теперь код:

export const AppRouter = () => {
  const router = createBrowserRouter(
    createRoutesFromElements(
      <Route
        path={ROUTES_PATHS.root}
        element={<RootPage />}
        errorElement={<RouteErrorBoundary />}
      >
        <Route
          path={ROUTES_PATHS.йте}
          element={<Breadcrumbs />}
          errorElement={<RouteErrorBoundary />}
          handle={{
            crumb: () => <Link to={ROUTES_PATHS.collections}>{'Коллекции'}</Link>,
          }}
        >
          <Route
            path={ROUTES_PATHS.books}
            element={<Outlet />}
            errorElement={<RouteErrorBoundary />}
            handle={{
              crumb: () => <Link to={ROUTES_PATHS.books}>{'Фигурки'}</Link>,
            }}
          >
            <Route
              path=':id'
              element={<BookCard />}
              errorElement={<RouteErrorBoundary />}
              handle={{
                crumb: (id: string) => (
                  <ApolloDataLink
                    getLinkText={(response) => response?.data.name ?? 'Имя книги отсутствует'}
                    to={`${ROUTES_PATHS.books}/${id}`}
                    useApolloLazyQuery={useGetBookByIdLazyQuery}
                    payload={id}
                  />
                ),
              }}
            />
            <Route
              path='*'
              element={<Navigate to={ROUTES_PATHS.books} />}
            />
          </Route>
          <Route
            path={ROUTES_PATHS.figurines}
            element={<Outlet />}
            errorElement={<RouteErrorBoundary />}
            handle={{
              crumb: () => <Link to={ROUTES_PATHS.figurines}>{'Фигурки'}</Link>,
            }}
          >
            <Route
              path=':id'
              element={<FigureCard />}
              errorElement={<RouteErrorBoundary />}
              handle={{
                crumb: (id: string) => (
                  <RestDataLink
                    getLinkText={(response) => response?.data.name ?? 'Имя фигурки отсутствует'}
                    to={`${ROUTES_PATHS.figurines}/${id}`}
                    useRestLazyQuery={useGetFigureByIdLazyQuery}
                    payload={id}
                  />
                ),
              }}
            />
            <Route
              path='*'
              element={<Navigate to={ROUTES_PATHS.figurines} />}
            />
          </Route>
          <Route
            path='*'
            element={<Navigate to={ROUTES_PATHS.books} />}
          />
        </Route>
        <Route
          path='*'
          element={<NotFoundPage />}
        />
      </Route>,
    ),
  );

  return <RouterProvider router={router} />;
};

Вся магия происходит в поле 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: (
          <Link to='/'>
            {'Главная'}
          </Link>
        ),
      },
      ...extraBreadcrumbItems,
    ],
    [extraBreadcrumbItems],
  );

  return (
    <>
      <Breadcrumb items={breadcrumbItems} />
      <Outlet />
    </>
  );
};

Явный минус, что не получится типизировать подобное «хранилище». Поэтому нужно позаботиться о проверках, дабы приложение случайно не упало, если вдруг забудем передать «крошку». Также если кому интересно, что из себя представляет компонент Breadcrumb, то это можно посмотреть в документации antd.

Осталось показать, что из себя представляют ApolloDataLink и RestDataLink - компоненты, которые делают сам запрос для получения названия «крошки».

ApolloDataLink:

interface IApolloDataLink<TData, TVariables extends Record<string, any>> {
  getLinkText: (data: TData | undefined) => string;
  to: string;
  useLazyQuery: () => Apollo.LazyQueryResultTuple<TData, TVariables>;
  variables: TVariables;
}

function ApolloDataLink<TData, TVariables extends Record<string, any>>(props: IApolloDataLinkLink<TData, TVariables>) {
  const { getLinkText, useLazyQuery, to, variables } = props;
  const [getData, { data }] = useLazyQuery();

  React.useEffect(() => {
    getData({ variables });
  }, [variables]); // eslint-disable-line react-hooks/exhaustive-deps

  return <Link to={to}>{getLinkText(data)}</Link>;
}

RestDataLink:

interface IRestDataLink<TData, TPayload> {
  getLinkText: (data: TData | undefined) => string;
  to: string;
  useRestLazyQuery: (
    payloadAsKey: TPayload,
  ) => [call: (payload: TPayload) => void, query: UseQueryResult<TData, TData>];
  payload: TPayload;
}

function RestDataLink<TData, TPayload>(props: IRestDataLink<TData, TPayload>) {
  const { getLinkText, useRestLazyQuery, to, payload } = props;
  const [getData, { data }] = useRestLazyQuery(payload);

  React.useEffect(() => {
    getData(payload);
  }, [payload]); // eslint-disable-line react-hooks/exhaustive-deps

  return <Link to={to}>{getLinkText(data)}</Link>;
}

Не спрашивайте, почему я использую сразу и Apollo и TanStack, так как это уже вопрос о переобувании авторов различных backend-сервисов в приложении, которое и породило идею этой статьи.

Но главное, что и Apollo и TanStack имеют кэш. Поэтому при отображении компонента конкретной сущности данные берутся уже из кэша.

Вроде вышло немного, но в свое время я посидел часа три над этой задачей. Надеюсь, что эта статья будет полезной. Счастливого кодинга!

P.S. В TanStack нет ленивых запросов подобных Apollo, но легко сделать, что-то подобное, прочитав вот этот раздел документации.

Tags:
Hubs:
Total votes 3: ↑3 and ↓0+3
Comments0

Articles