Skip to content

Instantly share code, notes, and snippets.

@remy

remy/ActiveLink.js

Last active Oct 17, 2020
Embed
What would you like to do?
Next.js version of `activeClassName` support.
import Link from './Link'; // our version of link

export default () => (
  <header className="Header">
    <nav>
      <Link activeClassName="active" href="/">
        <a className="some-other-class">Home</a>
      </Link>
      <Link activeClassName="active" href="/about">
        <a>About</a>
      </Link>
      <Link activeClassName="active" href="/contact">
        <a>Contact</a>
      </Link>
    </nav>
  </header>
);
@pojntfx

This comment has been minimized.

Copy link

@pojntfx pojntfx commented Mar 27, 2018

Awesome, thanks a lot! Here is my more minimal version for when the className does not have to be set using a prop:

import Link from "next/link";
import { withRouter } from "next/router";
import { Children } from "react";

const ActiveLink = withRouter(({ router, children, ...props }) => (
  <Link {...props}>
    {React.cloneElement(Children.only(children), {
      className: router.pathname === props.href ? `active` : null
    })}
  </Link>
));
@pojntfx

This comment has been minimized.

Copy link

@pojntfx pojntfx commented Mar 27, 2018

In case someone only needs to match the first url segment, try this:

const ActiveLink = withRouter(({ router, children, ...props }) => (
  <Link {...props}>
    {React.cloneElement(Children.only(children), {
      className:
        `/${router.pathname.split("/")[1]}` === props.href ? `active` : null
    })}
  </Link>
));
@LHIOUI

This comment has been minimized.

Copy link

@LHIOUI LHIOUI commented Jul 5, 2018

In case someone using as in links, just change pathname with asPath.

@peacefulseeker

This comment has been minimized.

Copy link

@peacefulseeker peacefulseeker commented Jan 6, 2019

zeti/next already have the use case with withRouter HOC.
https://github.com/zeit/next.js/tree/canary/examples/using-with-router

@emil-alexandrescu

This comment has been minimized.

Copy link

@emil-alexandrescu emil-alexandrescu commented Mar 12, 2019

Thanks a lot! Cleaned version of ActiveLink.

import { withRouter } from 'next/router';
import cx from 'classnames';
import Link from 'next/link';
import React, { Children } from 'react';

const ActiveLink = ({
  router,
  children,
  href,
  activeClassName,
  ...otherProps
}) => {
  const child = Children.only(children);
  const className = cx(child.props.className, {
    [activeClassName]: router.pathname === href && activeClassName
  });

  return (
    <Link href={href} {...otherProps}>
      {React.cloneElement(child, { className })}
    </Link>
  );
};
@hkirsman

This comment has been minimized.

Copy link

@hkirsman hkirsman commented May 1, 2019

What's that 'classnames' on second line? @emil-alexandrescu

@davideperozzi

This comment has been minimized.

Copy link

@davideperozzi davideperozzi commented Jul 22, 2019

Thank you! Here is my typescript version:

import cx from 'classnames';
import { WithRouterProps } from 'next/dist/client/with-router';
import NextLink from 'next/link';
import { withRouter } from 'next/router';
import { ReactElementLike } from 'prop-types';
import { Children, cloneElement, Component } from 'react';

interface LinkProps extends WithRouterProps {
  href: string;
  children: ReactElementLike;
  activeClassName: string;
}

class Link<P extends LinkProps> extends Component<P> {
  public render() {
    const {
      router,
      children,
      href,
      activeClassName,
      ...otherProps
    } = this.props;

    const child = Children.only(children);
    const active = this.props.router.pathname === href && activeClassName;
    const className = cx(
      child.props.className,
      { [activeClassName]: active }
    );

    return (
      <NextLink href={this.props.href} {...otherProps}>
        {cloneElement(child, { className })}
      </NextLink>
    );
  }
}

export default withRouter(Link);
@ScottSmith95

This comment has been minimized.

Copy link

@ScottSmith95 ScottSmith95 commented Jul 27, 2019

Here's a version using a Hook instead of a HOC.

import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";

const ActiveLink = ({ children, ...props }) => {
  const router = useRouter();

  const child = React.Children.only(children);

  let className = child.props.className || "";
  if (router.pathname === props.href && props.activeClassName) {
    className = `${className} ${props.activeClassName}`.trim();
  }

  delete props.activeClassName;

  return <Link {...props}>{React.cloneElement(child, { className })}</Link>;
};
@goodok21

This comment has been minimized.

Copy link

@goodok21 goodok21 commented Aug 7, 2019

Here's a version using a Hook and styled-components:

const ActiveLink = ({ children, ...props }) => {
  const router = useRouter()
  const child = React.Children.only(children)
  return (
    <Link {...props}>
      {React.cloneElement(child, { active: router.pathname === props.href })}
    </Link>
  )
}

Using example:

<ActiveLink href="/path" prefetch>
  <NavLink>Vikup auto</NavLink>
</ActiveLink>

NavLink components something like this:

const NavLink = styled.a`
  color: ${({ active }) => active ? 'red' : 'black'}; 
`
@JavierBrooktec

This comment has been minimized.

Copy link

@JavierBrooktec JavierBrooktec commented Aug 28, 2019

Hey guys im having an issue with dynamic routing and this component.

I have a Nav component in components folder:

import Link from './Link';

export default () => (
 <nav>
   <style jsx>{`
     .active {
       color: red;
     }

     .nav-link {
       text-decoration: none;
       padding: 10px;
       display: block;
     }
   `}</style>

   <ul>
     <li>
       <Link activeClassName="active" href="/">
         <a className="nav-link home-link">Home</a>
       </Link>
     </li>
     <li>
       <Link activeClassName="active" href="/about">
         <a className="nav-link">About</a>
       </Link>
     </li>
     <li>
       <Link activeClassName="active" href="/post/[id]" as="/post/first">
         <a>First Post</a>
       </Link>
     </li>
     <li>
       <Link activeClassName="active" href="/post/[id]" as="/post/second">
         <a>Second Post</a>
       </Link>
     </li>
   </ul>
 </nav>
);

and my pages folder look like this:

pages/
|---- post/
|      ---- [id]/
|             ---- index.js     
|---- about.js
|---- index.js

and the Index.js from [id] folder:

import {useRouter} from 'next/router';
import Nav from '../../../components/Nav';

const Post = () => {
  const router = useRouter();
  const {id} = router.query;

  return (
    <React.Fragment>
      <Nav />
      <h1>Post: {id}</h1>
    </React.Fragment>
  );
};

export default Post;

the problem it's that adds right the active class in al cases except if u reload in /post/[id], if so its return this error and don't add the active class:
Warning: Prop className did not match. Server: "jsx-382284644" Client: "jsx-382284644 active"

Captura de pantalla 2019-08-28 a las 19 22 23

in my Link component im comparin the asPath insted of href like this:

import {withRouter} from 'next/router';
import Link from 'next/link';
import React, {Children} from 'react';

const ActiveLink = ({router, children, as, ...props}) => {
  const child = Children.only(children);

  let className = child.props.className || null;

  if (as) {
    if (router.asPath === as && props.activeClassName) {
      className = `${className !== null ? className : ''} ${props.activeClassName}`.trim();
    }
  } else {
    if (router.pathname === props.href && props.activeClassName) {
      className = `${className !== null ? className : ''} ${props.activeClassName}`.trim();
    }
  }

  delete props.activeClassName;

  return (
    <Link as={as} {...props}>
      {React.cloneElement(child, {className})}
    </Link>
  );
};

export default withRouter(ActiveLink);
@canrozanes

This comment has been minimized.

Copy link

@canrozanes canrozanes commented Oct 11, 2019

A version with ES6 destructuring, proptypes and useRotuer hook:

import React from 'react';
import Link from 'next/link';
import PropTypes from 'prop-types';
import { useRouter } from 'next/router';

const ActiveLink = ({ href, activeClassName, children }) => {
  const router = useRouter();

  const child = React.Children.only(children);

  let className = child.props.className || '';
  if (router.pathname === href && activeClassName) {
    className = `${className} ${activeClassName}`.trim();
  }

  return <Link href={href}>{React.cloneElement(child, { className })}</Link>;
};

ActiveLink.propTypes = {
  href: PropTypes.string,
  activeClassName: PropTypes.string,
  children: PropTypes.node.isRequired
};
ActiveLink.defaultProps = {
  href: '',
  activeClassName: ''
};

export default ActiveLink;
@DominicTobias

This comment has been minimized.

Copy link

@DominicTobias DominicTobias commented Jan 27, 2020

I do it like this:

const ActiveLink: NextPage<Props> = ({ children, href }) => {
  const router = useRouter();

  return (
    <Link href={href} passHref>
      <Anchor active={router.pathname === href}>{children}</Anchor>
    </Link>
  );
};

const Anchor = styled.a<{ active: boolean }>`
  color: ${props => (props.active ? 'red' : 'blue')};
`;

If full control is needed just replace Link all together, from their docs:

import { useRouter } from 'next/router'

function ActiveLink({ children, href }) {
  const router = useRouter()
  const style = {
    marginRight: 10,
    color: router.pathname === href ? 'red' : 'black',
  }

  const handleClick = e => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink
@stfenjobs

This comment has been minimized.

Copy link

@stfenjobs stfenjobs commented Mar 14, 2020

new typescript version:

import { withRouter } from 'next/router';

const ActiveLink = ({ router, href, children }: any) => {
    (function prefetchPages() {
        if (typeof window !== 'undefined') {
            router.prefetch(router.pathname);
        }
    })();

    const handleClick = (event: React.MouseEvent<HTMLElement>) => {
        event.preventDefault();
        router.push(href);
  
    };

    const isCurrentPath = router.pathname === href || router.asPath === href;

    return (
        <a
            href={href}
            onClick={handleClick}
            className="url-item"
            style={{
                fontWeight: isCurrentPath ? 'bold' : 'normal',
                color: isCurrentPath ? '#F00' : '#fff'
            }}
        >
            {children}
        </a>
    );
};

export default withRouter(ActiveLink);
@shobhit-jain

This comment has been minimized.

Copy link

@shobhit-jain shobhit-jain commented Aug 16, 2020

I did something like this with typescript and tailwind css :

import { useRouter } from 'next/router'
import Link from 'next/link'

type Props = {
  href: string
  linkName: string
  activeClassName?: string
} & typeof defaultProps

const defaultProps = {
  activeClassName: 'text-green font-600',
}

export const NavLink = ({ href, linkName, activeClassName }: Props) => {
  const router = useRouter()

  return (
    <Link href={href}>
      <a className={router.pathname === href ? activeClassName : null}>
        {linkName}
      </a>
    </Link>
  )
}

NavLink.defaultProps = defaultProps
@nathansearles

This comment has been minimized.

Copy link

@nathansearles nathansearles commented Sep 16, 2020

If you need support for dynamic routes ([slug], [id], [...param], [[...slug]], etc.) in Next.js. The following will check if the href contains a dynamic route and then check if the "router.asPath" is equal to the "props.as". When matched the "activeClassName" will be added to the link.

Thanks @remy!

import { useRouter } from "next/router";
import Link from "next/link";
import React, { Children } from "react";

const ActiveLink = ({ children, ...props }) => {
  const router = useRouter();
  const child = Children.only(children);
  let className = child.props.className || "";
  const isDynamicRoute = props.href.match(/^\/?\[{1,2}\.{0,3}[a-z]+\]{1,2}$/);

  if (
    router.pathname === props.href &&
    !isDynamicRoute &&
    props.activeClassName
  ) {
    className = `${className} ${props.activeClassName}`.trim();
  } else if (router.asPath === props.as && isDynamicRoute) {
    className = `${className} ${props.activeClassName}`.trim();
  }

  delete props.activeClassName;

  return <Link {...props}>{React.cloneElement(child, { className })}</Link>;
};

export default ActiveLink;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.