Skip to content

Instantly share code, notes, and snippets.

@kachar
Last active May 6, 2024 21:38
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kachar/028b6994eb6b160e2475c1bb03e33e6a to your computer and use it in GitHub Desktop.
Save kachar/028b6994eb6b160e2475c1bb03e33e6a to your computer and use it in GitHub Desktop.
Next.js Link + Material UI Link/Button components bundled with forwardRef
import { Button, ButtonProps } from '@mui/material'
import NextLink from 'next/link'
export default function LinkButton(props: ButtonProps<'a'>) {
return <Button component={NextLink} {...props} />
}
// Plain JS version + prop-types
// Thanks to @IvanAdmaers
import PropTypes from 'prop-types';
import { forwardRef } from 'react';
import NextLink from 'next/link';
import { Button as MuiButton } from '@material-ui/core';
const LinkButton = forwardRef(({ href, as, prefetch, locale, ...props }, ref) => {
return (
<NextLink href={href} as={as} prefetch={prefetch} locale={locale} passHref>
<MuiButton buttonRef={ref} {...props} />
</NextLink>
);
});
LinkButton.displayName = 'LinkButton';
LinkButton.defaultProps = {
href: '#',
prefetch: false,
};
LinkButton.propTypes = {
href: PropTypes.string,
locale: PropTypes.string,
as: PropTypes.string,
prefetch: PropTypes.bool,
};
export default LinkButton;
import React, { forwardRef, Ref } from 'react'
import Link, { LinkProps } from 'next/link'
import { Button, ButtonProps } from '@material-ui/core'
type LinkRef = HTMLAnchorElement | HTMLButtonElement
type NextLinkProps = Omit<ButtonProps, 'href'> &
Pick<LinkProps, 'href' | 'as' | 'prefetch' | 'locale'>
const NextLink = ({ href, as, prefetch, locale, ...props }: LinkProps, ref: Ref<LinkRef>) => (
<Link href={href} as={as} prefetch={prefetch} locale={locale} passHref>
<Button buttonRef={ref} {...props} />
</Link>
)
export default forwardRef<LinkRef, NextLinkProps>(NextLink)
import React, { forwardRef, Ref } from 'react'
import Link, { LinkProps } from 'next/link'
import { Button, ButtonProps } from '@mui/material'
type LinkRef = HTMLButtonElement
type NextLinkProps = Omit<ButtonProps, 'href'> &
Pick<LinkProps, 'href' | 'as' | 'prefetch' | 'locale'>
const NextLink = ({ href, as, prefetch, locale, ...props }: LinkProps, ref: Ref<LinkRef>) => (
<Link href={href} as={as} prefetch={prefetch} locale={locale} passHref>
<Button ref={ref} {...props} />
</Link>
)
export default forwardRef<LinkRef, NextLinkProps>(NextLink)
// MaterialUI v5
// Thanks to @bryantobing12
import React from "react";
import { Link as LinkMUI, LinkProps as LinkMUIProps } from "@mui/material";
import NextLink, { LinkProps as NextLinkProps } from "next/link";
export type LinkProps = Omit<LinkMUIProps, "href" | "classes"> &
Pick<NextLinkProps, "href" | "as" | "prefetch">;
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
({ href, as, prefetch, ...props }, ref) => (
<NextLink href={href} as={as} prefetch={prefetch} passHref>
<LinkMUI ref={ref} {...props} />
</NextLink>
)
);
// @source https://github.com/mui/material-ui/tree/master/examples/nextjs-with-typescript
// Updated in this gist by @alecriarstudio
import * as React from 'react';
import clsx from 'clsx';
import { useRouter } from 'next/router';
import NextLink, { LinkProps as NextLinkProps } from 'next/link';
import MuiLink, { LinkProps as MuiLinkProps } from '@mui/material/Link';
import { styled } from '@mui/material/styles';
// Add support for the sx prop for consistency with the other branches.
const Anchor = styled('a')({});
type NextLinkComposedProps = {
to: NextLinkProps["href"];
linkAs?: NextLinkProps["as"];
} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> &
Omit<NextLinkProps, "href" | "as">;
export const NextLinkComposed = React.forwardRef<HTMLAnchorElement, NextLinkComposedProps>(
function NextLinkComposed(props, ref) {
const { to, linkAs, replace, scroll, shallow, prefetch, locale, ...other } = props;
return (
<NextLink
href={to}
prefetch={prefetch}
as={linkAs}
replace={replace}
scroll={scroll}
shallow={shallow}
passHref
locale={locale}
>
<Anchor ref={ref} {...other} />
</NextLink>
);
},
);
export type LinkProps = {
activeClassName?: string;
as?: NextLinkProps['as'];
href: NextLinkProps['href'];
linkAs?: NextLinkProps['as']; // Useful when the as prop is shallow by styled().
noLinkStyle?: boolean;
} & Omit<NextLinkComposedProps, 'to' | 'linkAs' | 'href'> &
Omit<MuiLinkProps, 'href'>;
// A styled version of the Next.js Link component:
// https://nextjs.org/docs/api-reference/next/link
const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link(props, ref) {
const {
activeClassName = 'active',
as,
className: classNameProps,
href,
linkAs: linkAsProp,
locale,
noLinkStyle,
prefetch,
replace,
role, // Link don't have roles.
scroll,
shallow,
...other
} = props;
const router = useRouter();
const pathname = typeof href === 'string' ? href : href.pathname;
const className = clsx(classNameProps, {
[activeClassName]: router.pathname === pathname && activeClassName,
});
const isExternal =
typeof href === 'string' && (href.indexOf('http') === 0 || href.indexOf('mailto:') === 0);
if (isExternal) {
if (noLinkStyle) {
return <Anchor className={className} href={href} ref={ref} {...other} />;
}
return <MuiLink className={className} href={href} ref={ref} {...other} />;
}
const linkAs = linkAsProp || as;
const nextjsProps = { to: href, linkAs, replace, scroll, shallow, prefetch, locale };
if (noLinkStyle) {
return <NextLinkComposed className={className} ref={ref} {...nextjsProps} {...other} />;
}
return (
<MuiLink
component={NextLinkComposed}
className={className}
ref={ref}
{...nextjsProps}
{...other}
/>
);
});
export default Link;
@ivanzotov
Copy link

Thanks!

@kachar
Copy link
Author

kachar commented Jan 20, 2021

@ivanzotov I've just updated to the latest version of the snippet I use.
The difference is in the type of LinkRef from any to more specific HTMLAnchorElement

Also added a version for next.js link + mui button

@ivanzotov
Copy link

@kachar Thank you so much, you've saved my day )

@ivanzotov
Copy link

@kachar
Copy link
Author

kachar commented Jan 21, 2021

@ivanzotov Thanks! I've updated it

Actually it seems this wasn't a problem locally because we always apply <LinkButton component="a" /> which makes the underlying component a link and it makes sense to passHref only to a link, not a button.

@JulianPorras8
Copy link

You won a BIG Start!

@minhoyooDEV
Copy link

wonderful ~!

@IvanAdmaers
Copy link

The same without typescript.

import PropTypes from 'prop-types';
import { forwardRef } from 'react';
import NextLink from 'next/link';
import { Link as MuiLink } from '@material-ui/core';

const Link = forwardRef(({ href, as, prefetch, ...props }, ref) => {
  return (
    <NextLink href={href} as={as} prefetch={prefetch} passHref>
      <MuiLink ref={ref} {...props} />
    </NextLink>
  );
});

Link.displayName = 'Link';

Link.defaultProps = {
  href: '#',
  prefetch: false,
};

Link.propTypes = {
  href: PropTypes.string,
  as: PropTypes.string,
  prefetch: PropTypes.bool,
};

export default Link;

@IvanAdmaers
Copy link

LinkButton Plain Js

import PropTypes from 'prop-types';
import { forwardRef } from 'react';
import NextLink from 'next/link';
import { Button as MuiButton } from '@material-ui/core';

const Button = forwardRef(({ href, as, prefetch, locale, ...props }, ref) => {
  return (
    <NextLink href={href} as={as} prefetch={prefetch} locale={locale} passHref>
      <MuiButton buttonRef={ref} {...props} />
    </NextLink>
  );
});

Button.displayName = 'Button';

Button.defaultProps = {
  href: '#',
  prefetch: false,
};

Button.propTypes = {
  href: PropTypes.string,
  locale: PropTypes.string,
  as: PropTypes.string,
  prefetch: PropTypes.bool,
};

export default Button;

@bryanltobing
Copy link

Mui v5
Example with ts to support passing LinkMuiProps like variant color etc

import React from "react";
import { Link as LinkMUI, LinkProps as LinkMUIProps } from "@mui/material";
import NextLink, { LinkProps as NextLinkProps } from "next/link";

export type LinkProps = Omit<LinkMUIProps, "href" | "classes"> &
  Pick<NextLinkProps, "href" | "as" | "prefetch">;

export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
  ({ href, as, prefetch, ...props }, ref) => (
    <NextLink href={href} as={as} prefetch={prefetch} passHref>
      <LinkMUI ref={ref} {...props} />
    </NextLink>
  )
);

@kachar
Copy link
Author

kachar commented Mar 10, 2022

Thanks @bryantobing12
Added your v5 contribution to the gist

@IvanAdmaers
Copy link

There is a cool example of usage NextJS Link and MUI 5 Link components together in the official MUI repository - https://github.com/mui/material-ui/blob/master/examples/nextjs/src/Link.js

@alecriarstudio
Copy link

OfficialLinkMuiv5.tsx

interface NextLinkComposedProps
  extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>,
    Omit<NextLinkProps, 'href' | 'as'> {
  to: NextLinkProps['href'];
  linkAs?: NextLinkProps['as'];
}

The following 2 errors was displayed at interface NextLinkComposedProps on VSCode

Interface 'NextLinkComposedProps' cannot simultaneously extend types 'Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href">' and 'Omit<InternalLinkProps, "href" | "as">'.
  Named property 'onClick' of types 'Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href">' and 'Omit<InternalLinkProps, "href" | "as">' are not identical. ts(2320)
Interface 'NextLinkComposedProps' cannot simultaneously extend types 'Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href">' and 'Omit<InternalLinkProps, "href" | "as">'.
  Named property 'onMouseEnter' of types 'Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href">' and 'Omit<InternalLinkProps, "href" | "as">' are not identical. ts(2320)

The following are candidates for correction

type NextLinkComposedProps = {
  to: NextLinkProps["href"];
  linkAs?: NextLinkProps["as"];
} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href"> &
  Omit<NextLinkProps, "href" | "as">;

@kachar
Copy link
Author

kachar commented Jun 8, 2022

Thanks for the suggestion @alecriarstudio

I've updated this gist for anyone, but the official version is at https://github.com/mui/material-ui/tree/master/examples/nextjs-with-typescript

You might send a PR to the MUI repo to adapt your suggestion as well.

@kachar
Copy link
Author

kachar commented Dec 22, 2022

With the update brought by Next13 we're able to simplify the next/link + mui link/button process a lot:

import { Button, ButtonProps } from '@mui/material'
import NextLink from 'next/link'

export default function LinkButton(props: ButtonProps<'a'>) {
  return <Button component={NextLink} {...props} />
}
import { LinkProps, Link as MuiLink } from '@mui/material'
import NextLink from 'next/link'

export default function Link(props: LinkProps<'a'>) {
  return <MuiLink component={NextLink} {...props} />
}

Read more at https://stackoverflow.com/a/74419666/668245

@vighnesh153
Copy link

vighnesh153 commented Dec 29, 2022

Thanks @kachar

Latest approach is indeed simple but we are not able to pass href as an object. It only accepts string type. Given that NextLink accepts a URL object as href, how can we get same behaviour here?

@kachar
Copy link
Author

kachar commented Dec 29, 2022

@vighnesh153 I've tried to make it work with LinkProps['href'] but the MuiButton expectation is a hardcoded { href: string } so I don't think it's currently possible:

// node_modules/@mui/material/Button/Button.d.ts

export type ExtendButton<M extends OverridableTypeMap> = ((
  props: { href: string } & OverrideProps<ExtendButtonBaseTypeMap<M>, 'a'>,
) => JSX.Element) &
  OverridableComponent<ExtendButtonBaseTypeMap<M>>;

What I end up doing in multiple projects is having a general routes.ts file that contains all the app links as follows:

export const routes = {
  post: {
    byId: (id: string) => `/post/${id}`,
  }
}

and use it as:

<LinkButton color="secondary" variant="contained" href={routes.post.byId(query.id)}>
  View post
</LinkButton>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment