Material-ui: Convert ListItem compatible with react-router

Created on 30 Aug 2017  ·  21Comments  ·  Source: mui-org/material-ui

I want to make each ListItem in a List to a Link component.

I have a Drawer component composed of a List component,
That List is composed of 4 ListItems.
What I want to achieve is that convert each ListItem to a React router Link.
I tried to modify drawer's onClick method, get Id of the clicked ListItem and then programmatically change location of window based on the id.
But I do think this approach is more jqueryish that the react concepts.
I also tried to add Link to component prop in ListItem, but it resulted error.( I've seen you have used a in component prop in docs).
What is the recommended way to achieve this requirement?

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withStyles } from 'material-ui/styles';
import Drawer from 'material-ui/Drawer';
import List, { ListItem, ListItemIcon, ListItemText } from 'material-ui/List';
import Face from 'material-ui-icons/Face';
import Person from 'material-ui-icons/Person';
import Assignment from 'material-ui-icons/Assignment';
import Link from 'react-router-dom/Link';
import { FormattedMessage } from 'react-intl';

const styles = {
   list: {
      width: 250,
      flex: 'initial',
    },
};

 class UndockedDrawer extends Component {

   constructor() {
      super();
      this.onClick = this.onClick.bind(this);
    }

    onClick(e, data) {
       console.log(data);
       this.props.onRequestClose();
     }

   render() {
       const classes = this.props.classes;
        const sidebarListItems = (
     <div>
         <ListItem button >
            <ListItemIcon>
               <Person />
           </ListItemIcon>
            <ListItemText primary={<FormattedMessage id="user" />} />
          </ListItem>
    <ListItem button>
      <ListItemIcon>
        <Assignment />
      </ListItemIcon>
      <ListItemText primary={<FormattedMessage id="consultant" />} />
    </ListItem>
    <ListItem button>
      <ListItemIcon>
        <Face />
      </ListItemIcon>
      <ListItemText primary={<FormattedMessage id="student" />} />
    </ListItem>
  </div>
);

const sidebarList = (
  <div>
    <List className={classes.list} >
      {sidebarListItems}
    </List>
  </div>
);

return (
  <div>
    <Drawer
      anchor="right"
      open={this.props.open}
      onRequestClose={this.props.onRequestClose}
      onClick={this.onClick}
    >
      {sidebarList}
    </Drawer>
   </div>
   );
   }
 }

  UndockedDrawer.propTypes = {
     classes: PropTypes.shape({}).isRequired,
     open: PropTypes.bool.isRequired,
     onRequestClose: PropTypes.func.isRequired,
    };

   export default withStyles(styles)(UndockedDrawer);
  • Material-UI: 1.0.6-beta
  • React: ^15.6.1
  • Browser: Chrome: 60
  • react-router-dom : ^4.1.2
question

Most helpful comment

This is covered in the documentation: https://material-ui.com/components/buttons/#third-party-routing-library.

import React from 'react';
import { MemoryRouter as Router } from 'react-router';
import { Link, LinkProps } from 'react-router-dom';
import ListItem from '@material-ui/core/ListItem';
import { Omit } from '@material-ui/types';

// The usage of React.forwardRef will no longer be required for react-router-dom v6.
// see https://github.com/ReactTraining/react-router/issues/6056
const AdapterLink = React.forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => (
  <Link innerRef={ref as any} {...props} />
));

const CollisionLink = React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'innerRef' | 'to'>>(
  (props, ref) => <Link innerRef={ref as any} to="/getting-started/installation/" {...props} />,
);

export default function ButtonRouter() {
  return (
    <Router>
      <ListItem color="primary" component={AdapterLink} to="/">
        Simple case
      </ListItem>
      <ListItem component={CollisionLink}>Avoids props collision</ListItem>
    </Router>
  );
}

All 21 comments

I figured out, I can wrap ListItem in a Link as follow:

<Link to="users" style={{ textDecoration: 'none' }}>
      <ListItem button >
        <ListItemIcon>
          <Person />
        </ListItemIcon>
        <ListItemText primary={<FormattedMessage id="user" />} />
      </ListItem>
    </Link>

With this approach, the onClick method will be fired too.
Any suggestions or imporvements?

I figured out, I can wrap ListItem in a Link as follow

This solution isn't semantically valid. Have a look at how we handle the issue on the documentation:

https://github.com/callemall/material-ui/blob/92bd073015b9cc0dff3a26195fcb49153ddaab78/docs/src/modules/components/AppDrawerNavItem.js#L71-L83

You can also try this one

https://github.com/callemall/material-ui/blob/92bd073015b9cc0dff3a26195fcb49153ddaab78/docs/src/pages/demos/buttons/FlatButtons.js#L40-L42

This is covered in the documentation: https://material-ui.com/components/buttons/#third-party-routing-library.

import React from 'react';
import { MemoryRouter as Router } from 'react-router';
import { Link, LinkProps } from 'react-router-dom';
import ListItem from '@material-ui/core/ListItem';
import { Omit } from '@material-ui/types';

// The usage of React.forwardRef will no longer be required for react-router-dom v6.
// see https://github.com/ReactTraining/react-router/issues/6056
const AdapterLink = React.forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => (
  <Link innerRef={ref as any} {...props} />
));

const CollisionLink = React.forwardRef<HTMLAnchorElement, Omit<LinkProps, 'innerRef' | 'to'>>(
  (props, ref) => <Link innerRef={ref as any} to="/getting-started/installation/" {...props} />,
);

export default function ButtonRouter() {
  return (
    <Router>
      <ListItem color="primary" component={AdapterLink} to="/">
        Simple case
      </ListItem>
      <ListItem component={CollisionLink}>Avoids props collision</ListItem>
    </Router>
  );
}

I don't see it in the docs:
https://material-ui-next.com/demos/lists/
https://material-ui-next.com/demos/drawers/

This is a common thing when using React Router, but it's not clear how to use NavLink in a ListItem.
A Button with component prop?
And the button props and the component props are mixed together on the same JSX tag?
What is this sort of mixin?
Why so complex?

When using @mehrdaad 's solution the links look like normal MUI list, but when using component={Link} as @oliviertassinari suggested, the style gets messed up:
(In the image the mouse is hovering over "Build" but the second item gets the underline...)

screen shot 2017-11-16 at 12 09 05

@ilyador This UI link style issue has been solved.

@zhangwei900808 No, that's still wrong semantically, it generates <ul><a ..>...
https://codesandbox.io/s/lpwq74p30m

Just use the answer provided above

@oliviertassinari Would there be an easier way to do:

const styles = {
    li: {
        '&:hover': {
            backgroundColor: 'transparent',
        },
    },
};
// ...
<MenuItem disableGutters className={classes.li}>
    <Button component={Link} to="/logout" ...

To avoid a double hover effect, on the MenuItem and on Button (which still have a slight gutter/padding, another issue)
Because this is cumbersome

demo: https://codesandbox.io/s/nk834yk5pl (not using component={Link} to="/..." but that's the same)

@caub Why are you introducting an extra Button component? You can use the component property on the MenuItem.

@oliviertassinari because ul > a isn't valid html5 https://codesandbox.io/s/lpwq74p30m

@caub nav > a is valid.

Here is the solution that solved my issue

<List>
        {menus.map((menu: any, index: any) => {
          return (
            <ListItem
              key={index}
              {...{ to: menu.link }}
              component={Link}
              button={true}
            >
              <ListItemIcon>{menu.icon}</ListItemIcon>
              <ListItemText primary={menu.text} />
            </ListItem>
          );
        })}
      </List>

Thus just use ListItem as
key={index}
**{...{ to: menu.link }}**
component={Link}
button={true}
>

import {Link} from 'react-router-dom'

<ListItem component={Link} to="/" button>

// or
<ListItem component={props => <Link to="/" {...props} />} button>

@mehrdaad @oliviertassinari It's worked^_^

This was perfect for me. Thanks!

The most voted answer failed after the MUI v4 migration (I'm on 4.3.1) due to "Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"

I've replaced the NavLink component to a wrapper like so:

import React, { Component } from "react"
import { NavLink } from "react-router-dom"

/**
 * React Router Nav Link wrapper to forward the ref to fix "Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"
 *
 * From https://material-ui.com/guides/composition/#caveat-with-refs
 */
class NavLinkMui extends Component {
    render() {
        const { forwardedRef, ...props } = this.props
        return <NavLink {...props} ref={forwardedRef} />
    }
}

export default NavLinkMui

Usage:

<ListItem
    button
    component={NavLinkMui}
    to='/'
>
    <ListItemIcon>
        <SlideshowIcon />
    </ListItemIcon>
</ListItem>

@HugoGresse Yes, you need to use the forward ref API. You should be able to find examples in the documentation. I have updated https://github.com/mui-org/material-ui/issues/7956#issuecomment-326908167 with an up-to-date answer. Let us know if it's OK.

@oliviertassinari it did.

I'm still having some issue with either the forwardRef solution or Component one where the Button text color is not taking into account the theme palette color. The text color should be white to match palette.type: 'dark', but is white.

Probably linked to a change on MUI because without the component props I'm still having the issue.
Capture d'écran 2019-08-06 16 54 51

For those reading up here, you can pass additional props to the ListItemText...

<ListItemText
    primaryTypographyProps={{
        color: 'textPrimary'
    }}
    primary="Dashboard"
/>

BTW, your example is now in TS.

Here is the solution that solved my issue

<List>
        {menus.map((menu: any, index: any) => {
          return (
            <ListItem
              key={index}
              {...{ to: menu.link }}
              component={Link}
              button={true}
            >
              <ListItemIcon>{menu.icon}</ListItemIcon>
              <ListItemText primary={menu.text} />
            </ListItem>
          );
        })}
      </List>

Thus just use ListItem as
key={index}
**{...{ to: menu.link }}**
component={Link}
button={true}
>

button={true} simply makes the UI behavior of drawers the same as no link.

For others trying to do this in Typescript:

A simple example using the ListItem component property to specify a element as the ListItem implementation. The trick in Typescript is to use the spread operator (...) on the props of the ListItem to sneak past the unwanted and unneeded Typescript errors that specifying this type of component and "to" properties for the ListItem would normally bring about (Typescript error TS2769 Property 'component' does not exist on type 'IntrinsicAttributes...)

import { NavLink } from "react-router-dom";

        <List>
            <ListItem button={true} {...{ component: NavLink, to: "/Somewhere" }}>
                <ListItemIcon>
                    <ShoppingCartIcon />
                </ListItemIcon>
                <ListItemText primary="Orders" />
            </ListItem>
        </List>

or alternatively if you just want to use an onClick function on the ListItem for example:


    <List>
        <ListItem button={true} {...{ onClick: () => alert("foo") }}>
            <ListItemIcon>
                <DashboardIcon />
            </ListItemIcon>
            <ListItemText primary="Dashboard" />
        </ListItem>
    </List>

```

This worked for me

const NavLinkMui = React.forwardRef((props, ref) => (
  <NavLink {...props} activeClassName="Mui-selected" ref={ref} />
))
<ListItem button component={NavLinkMui} to={to}>{text}</ListItem>

The most voted answer failed after the MUI v4 migration (I'm on 4.3.1) due to "Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"

I've replaced the NavLink component to a wrapper like so:

import React, { Component } from "react"
import { NavLink } from "react-router-dom"

/**
 * React Router Nav Link wrapper to forward the ref to fix "Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?"
 *
 * From https://material-ui.com/guides/composition/#caveat-with-refs
 */
class NavLinkMui extends Component {
    render() {
        const { forwardedRef, ...props } = this.props
        return <NavLink {...props} ref={forwardedRef} />
    }
}

export default NavLinkMui

Usage:

<ListItem
    button
    component={NavLinkMui}
    to='/'
>
    <ListItemIcon>
        <SlideshowIcon />
    </ListItemIcon>
</ListItem>

Thanks @HugoGresse This works perfectly well :-)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

reflog picture reflog  ·  3Comments

iamzhouyi picture iamzhouyi  ·  3Comments

zabojad picture zabojad  ·  3Comments

revskill10 picture revskill10  ·  3Comments

anthony-dandrea picture anthony-dandrea  ·  3Comments