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

c3183c67615b92f7590463a1b3ba3c3e.png

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

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

Я решал эту задачу в 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, но легко сделать, что-то подобное, прочитав вот этот раздел документации.

© Habrahabr.ru