Как нацелить и стилизовать активную ссылку в Next.Js (с помощью Typescript)

Большинство проектов React используют activeClassName React Router для нацеливания активных маршрутов. Но создание навигационного компонента в Next.JS со стилизованной активной ссылкой не так просто.

В Next.js встроенный компонент <Link> требует настройки для достижения подобного эффекта.

Давайте рассмотрим два решения с использованием Typescript: базовое и подробное (рекомендуемое).

Базовое решение

Это пример базового решения, в котором используется пользовательский компонент ActiveLink и хук useRouter.

//Set up your navigation component with a custom 'ActiveLink' component (imported) from a separate file.
// Then create a page route file and component for each 'href' path i.e. index.tsx, about.tsx, products.tsx 

import ActiveLink from './ActiveLink';

const Nav = () => {
  return (
    <nav>
      <ul className="nav">
        <li>
          <ActiveLink href="/">
            Home
          </ActiveLink>
        </li>
        <li>
          <ActiveLink href="/about">
            About
          </ActiveLink>
        </li>
        <li>
          <ActiveLink
            href="/products/"
          >
            Products
          </ActiveLink>
        </li>     
      </ul>
    </nav>
  );
};

export default Nav;


Вход в полноэкранный режим Выход из полноэкранного режима

Далее, давайте создадим компонент ActiveLink, чтобы воссоздать поведение активной ссылки.

import { useRouter } from 'next/router'
import { LinkProps } from 'next/link';

//LinkProps is a type that requires 'href' as a prop. We're extending it to include a react element as a children prop.
type ActiveLinkProps = LinkProps & {
  children: ReactElement;
}

// href is the url path passed as a prop in the Nav component. The children are the string names passed in-between the ActiveLink tags.
function ActiveLink({ children, href }: ActiveLinkProps) {

// Deconstruct `asPath` from the router object to access the current page path shown in your browser (including the search params).
  const {asPath} = useRouter()

  //define the styling for the active link. If the current page path matches the 'href' path provided, display a red link. All other links will be black.
  const style = {
    color: asPath === href ? 'red' : 'black',
  }

  // Navigate to the page provided as 'href' when the link is clicked (router.push is used for client-side transitions)
  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  //the active link will have a style of 'color:red' 
  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink
Вход в полноэкранный режим Выход из полноэкранного режима

Это достойное решение. Но что, если мы хотим расширить наше приложение и включить в него рендеринг на стороне сервера, динамические маршруты, пользовательские реквизиты ссылок и многое другое?

Вот несколько дополнительных настроек нашего компонента ActiveLink:

Рекомендуемое решение

Во-первых, в компоненте Nav добавьте реквизит activeClassName со строкой active в компонент ActiveLink каждого маршрута страницы.

Вы также можете добавить динамический маршрут «catch-all» для вложенных страниц внутри /products, т.е. /products/categories. Обязательно создайте соответствующие маршруты страниц в папке pages следующим образом:

  • pages
    • products
      • […slug] // страница по умолчанию для всех маршрутов «catch-all»
      • index.tsx // главная страница по умолчанию для /products

import ActiveLink from './ActiveLink';

const Nav = () => {
  return (
    <nav>
      <ul className="nav">
        <li>
          <ActiveLink activeClassName="active" href="/">
            <a>Home</a>
          </ActiveLink>
        </li>
        .....
        //add the 'activeClassName' to each ActiveLink as shown in the previous section.
       ...... 

       // this is an example of a dynamic route using query paramaters.
        <li>
          <ActiveLink
            activeClassName="active"
            href="/products/[...slug]"
            as="/products/categories?limit=5"
          >
            <a>Products Categories </a>
          </ActiveLink>
        </li>
      </ul>
    </nav>
  );
};

export default Nav;

Вход в полноэкранный режим Выход из полноэкранного режима

Во-вторых, давайте переделаем наш компонент ActiveLink, чтобы учесть реквизит activeClassName и дополнительные реквизиты, которые вы можете передать в будущем.

Нам также нужно убедиться, что asPath из хука useRouter не приведет к несоответствию маршрутов, если страница рендерится с помощью рендеринга на стороне сервера.

Чтобы избежать этого, Next.js docs рекомендует использовать isReady: булево значение, используемое для проверки того, обновляются ли поля маршрутизатора на стороне клиента.

import { useRouter } from 'next/router';
import Link, { LinkProps } from 'next/link';
import React, { useState, useEffect, ReactElement, Children } from 'react';

//Add the activeClassName as a required prop
type ActiveLinkProps = LinkProps & {
  children: ReactElement;
  activeClassName: string;
};

const ActiveLink = ({
  children,
  activeClassName,
  ...props
}: ActiveLinkProps) => {

  //deconstruct 'isReady' from the useRouter hook.
  const { asPath, isReady } = useRouter();

  //create an empty string as the default className of the component
  const [className, setClassName] = useState('');

  useEffect(() => {
    // isReady checks if the router fields are updated client-side (it must be used inside a useEffect hook)
    if (isReady) {

      // URL().pathname will help to eliminate query and hash strings from the url. 
      // Props.as targets dynamic routes, whilst props.href targets normal static page routes.

      const linkPathname = new URL(
        (props.as || props.href) as string,
        location.href
      ).pathname;

      // Here we make use of 'asPath' in the correct context (once 'isReady' is true)
      const activePathname = new URL(asPath, location.href).pathname;


      // Attach the activeClassName to the matching current page 
      const newClassName =
        linkPathname === activePathname
          ? `${activeClassName}`: '';

      // Sets a new 'className' state if there is a mismatch between the current and previous state. This ensures a 'toggle' like behavior between link changes.
      if (newClassName !== className) {
        setClassName(newClassName);
      }
    }
// useEffect dependencies defined below
  }, [
    asPath,
    isReady,
    props.as,
    props.href,
    activeClassName,
    setClassName,
    className,
  ]);

  return (
     // return the in-built Next Link including a child (a clone the 'a' element (child) including the activeClassName if it is the active page)
    <Link {...props}>
      {React.cloneElement(child, {
        className: className || null,
      })}
    </Link>
  );
};

export default ActiveLink;



Войти в полноэкранный режим Выход из полноэкранного режима

Наконец, добавьте стиль в .active в глобальную таблицу стилей css (обычно импортируется в _app tsx).


.active {
  color: red;
}

.active:after {
  content: ' (current page)';
}

Вход в полноэкранный режим Выйдите из полноэкранного режима

Вы должны увидеть что-то вроде этого…

Обзор

Простое решение для определения цели и стиля активной ссылки в Next.Js заключается в создании пользовательского компонента Link, который использует хук useRouter для доступа к текущему пути и возвращает компонент Link с activeClassName.

Это activeClassName может быть стилизовано с помощью css для отображения активной ссылки маршрута страницы.

Оцените статью
devanswers.ru
Добавить комментарий