Material-ui adding Link component from react-router

JavascriptReactjsReact RouterMaterial Ui

Javascript Problem Overview


I'm struggling to add <Link/> component to my material-ui AppBar

This is my navigation class:

class Navigation extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    var styles = {
      appBar: {
        flexWrap: 'wrap'
      },
      tabs: {
        width: '100%'
      }
    }

    return (
      <AppBar showMenuIconButton={false} style={styles.appBar}>
        <Tabs style={styles.tabs}>
          <Tab label='Most popular ideas'/>
          <Tab label='Latest ideas' />
          <Tab label='My ideas' />
        </Tabs>
      </AppBar>
    )
  }
}

Which looks okay: Navbar

Tabs are clickable, have fluid animations, that's cool. But how do I wire them up together with react-router and its' <Link/> component?

I've tried adding onChange listener like that:

<Tab
  label='My ideas'
  onChange={<Link to='/myPath'></Link>}
/>

However I'm getting following error:

Uncaught Invariant Violation: Expected onChange listener to be a function, instead got type object

If I try to wrap <Tab/> component into <Link/> component, I'm getting error that <Tabs/> component accepts only <Tab/> component.

This doesn't work either (no error is being produced, but clicking on Tab does not bring me to the path):

<Tab label='Most popular ideas'>
  <Link to='/popular'/>
</Tab>

How do I make <Link/> component work together with <Tabs> and <AppBar>? If that's not possible, I can use any other component from material-ui library to form a proper menu.

Javascript Solutions


Solution 1 - Javascript

For Material UI 1.0 with Typescript: see this post by @ogglas below.

For Material-UI 1.0 with plain JS:

<Tabs value={value} onChange={this.handleChange}>
  {
    this.props.tabs.map(
      ({label, path})=><Tab key={label} 
                            label={label} 
                            className={classes.tabLink} 
                            component={Link} 
                            to={path} />
    )
  }
</Tabs>

And classes.tabLink is defined as:

tabLink : {
    display:"flex",
    alignItems:"center",
    justifyContent:"center"
}

How this works?

All the mui 1.0 components inheriting from ButtonBase, support a component prop, see ButtonBase. The idea is to allow you to control what the component renders as its wrapper/root element. Tab also has this feature although at the time of writing this answer this prop is not documented explicitly, but as Tab inherits from ButtonBase, all its props carry over (and the documentation does cover this).

Another feature of ButtonBase is that all the extra props, not in use by ButtonBase or inherited component, are spread over the specified component. We have used this behavior to send the to prop used by Link by giving it to Tab control. You can send any additional props in the same way. Note that this is documented explicitly for both ButtonBase and Tab.

Thanks @josh-l for asking this to be added.

Solution 2 - Javascript

here's how you can do it now:

<Tabs onChange={this.changeTab} value={value}>
   <Tab value={0} label="first" containerElement={<Link to="/first"/>} />
   <Tab value={1} label="second" containerElement={<Link to="/second"/>}/>
   <Tab value={2} label="third" containerElement={<Link to="/third"/>} />
 </Tabs>

Solution 3 - Javascript

You can try this simple method

 <Tab label='Most popular ideas'  to='/myPath' component={Link} />

Solution 4 - Javascript

This is solved using the <Link /> from material-ui instead of directly using the <Link /> or <NavLink /> from react-router. The example for the same can be found in the documentation here.

https://material-ui.com/components/links/

Also <Button /> tag has a component prop to achieve this

<Button color="inherit" component={Link} to={"/logout"}>Logout</Button>

An extensive discussion on this can be found here

https://github.com/mui-org/material-ui/issues/850

Solution 5 - Javascript

Since we are using TypeScript I could not use @hazardous solutions. This is how we implemented routing for material-ui v1.0.0-beta.16 and react-router 4.2.0. The reason why we are splitting this.props.history.location.pathname is because we need to access /renewals/123 for example. If we did not do this we would get the following warning and no tab would be displayed as active: Warning: Material-UI: the value provided '/renewals/123' is invalid

Complete code with imports:

import * as React from "react";
import * as ReactDOM from "react-dom";
import * as ReactRouter from "react-router";
import * as PropTypes from "prop-types";
import { Switch, Route, Redirect, Link  } from "react-router-dom";
import { Cases } from './../Cases';
import { SidePane } from './../SidePane';
import { withStyles, WithStyles } from 'material-ui/styles';
import Paper from 'material-ui/Paper';
import Tabs, { Tab } from 'material-ui/Tabs';
import { withRouter } from "react-router-dom";
import Badge from 'material-ui/Badge';
import Grid from 'material-ui/Grid';
import { Theme } from 'material-ui/styles';
import SimpleLineIcons from '../../Shared/SimpleLineIcons'

interface IState {
    userName: string;
}

interface IProps {
    history?: any
}

const styles = (theme: Theme) => ({
    root: theme.typography.display1,
    badge: {
        right: '-28px',
        color: theme.palette.common.white,
    },
    imageStyle:{
        float: 'left',
        height: '40px',
        paddingTop: '10px'
    },
    myAccount: {
        float: 'right'
    },
    topMenuAccount: {
        marginLeft: '0.5em',
        cursor: 'pointer'
    }
});

type WithStyleProps = 'root' | 'badge' | 'imageStyle' | 'myAccount' | 'topMenuAccount';

class Menu extends React.Component<IProps & WithStyles<WithStyleProps>, IState> {
    constructor(props: IProps & WithStyles<WithStyleProps>) {
        super(props);
        this.state = {
            userName: localStorage.userName ? 'userName ' + localStorage.userName : ""
        }
    }
    componentDidMount() {
        this.setState({ userName: localStorage.userName ? localStorage.userName : "" })
    }
    logout(event: any) {
        localStorage.removeItem('token');
        window.location.href = "/"
    }

    handleChange = (event: any, value: any) => {
        this.props.history.push(value);
    };

    render() {
        const classes = this.props.classes;
        let route = '/' + this.props.history.location.pathname.split('/')[1];
        return (
            <div>
                <Grid container spacing={24}>
                    <Grid item xs={12} className={classes.root}>
                        <img src="/Features/Client/Menu/logo.png" alt="Logo" className={classes.imageStyle} />
                        <div className={this.props.classes.myAccount}>
                        <span><span className={this.props.classes.topMenuAccount}>MY ACCOUNT</span><span className={classes.topMenuAccount}><SimpleLineIcons iconName={'user'} />&#x25BE;</span></span>
                            <span onClick={this.logout} className={classes.topMenuAccount}><SimpleLineIcons iconName={'logout'} /></span>
                        </div>
                    </Grid>
                    <Grid item xs={12} >
                        <div className="route-list">
                            <Tabs
                                value={route}
                                onChange={this.handleChange}
                                indicatorColor="primary"
                                textColor="primary"
                            >
                                <Tab label="Overview" value="/" />
                                <Tab label={<Badge classes={{ badge: classes.badge }} badgeContent={this.props.caseRenewalCount} color="primary">
                                    Renewals
                                   </Badge>} value="/renewals" />
                            </Tabs>
                        </div>
                    </Grid>
                </Grid>
            </div>
        );
    }
}
export default withStyles(styles)(withRouter(Menu))

Solution 6 - Javascript

TypeScript implementation of the router-driven tabs.

For those who look for the TypeScript implementation. Easy configurable. Driven by tabs configuration.

interface ITabsPageProps {
  match: match<{page: string}>;
  history: History;
}

const tabs = [{
  label: 'Fist Tab',
  link: 'fist-tab',
  component: <FirstTabContent/>
}, {
  label: 'Second Tab',
  link: 'second-tab',
  component: <SecondTabContent/>
}, {
  label: 'Third Tab',
  link: 'third-tab',
  component: <ThirdTabContent/>
}];

export class TabsPage extends React.Component<ITabsPageProps> {
  handleChange(tabLink: string) {
    this.props.history.push(`/tabs-page/${tabLink}`);
  }

  render() {
    const currentTab = this.props.match.params.page;
    const selectedTab = tabs.find(tab => currentTab === tab.link);

    return (
      <Fragment>
        <Tabs
          value={currentTab}
          onChange={(event, value) => this.handleChange(value)}
        >
          {tabs.map(tab => (
            <Tab
              key={tab.link}
              value={tab.link}
              label={tab.label}
            />
          ))}
        </Tabs>

        {selectedTab && selectedTab.component}
      </Fragment>
    );
  }
}

Solution 7 - Javascript

Here's another implementation of React with hooks, Material-UI with tabs, React Router with Link, and TypeScript.

import * as React from "react";
import { BrowserRouter as Router, Route, Redirect, Switch, Link, LinkProps } from 'react-router-dom';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import { default as Tab, TabProps } from '@material-ui/core/Tab';

import Home from './Home';
import ProductManagement from './ProductManagement';
import Development from './Development';
import HomeIcon from '@material-ui/icons/Home';
import CodeIcon from '@material-ui/icons/Code';
import TimelineIcon from '@material-ui/icons/Timeline';

const LinkTab: React.ComponentType<TabProps & LinkProps> = Tab as React.ComponentType<TabProps & LinkProps>;

function NavBar() {
  const [value, setValue] = React.useState(0);

  const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
    setValue(newValue);
  };
  return (
    <div >
      <AppBar position="static" >
        <Tabs value={value} onChange={handleChange} centered>
          <LinkTab label='Home' icon={ <HomeIcon />} component={Link} to="/" />
          <LinkTab label='Development' icon={<CodeIcon />} component={Link} to="/dev" />
          <LinkTab label='Product Management' icon={<TimelineIcon />} component={Link} to="/pm" />
        </Tabs>
      </AppBar>
    </div>
  )
};

export default function App() {
  return (
    <Router>
      <div>
        <NavBar />
        <Switch>
          <Route exact path="/" component={ Home } />
          <Route exact path="/dev" component={ Development } />
          <Route exact path="/pm" component={ ProductManagement } />
          <Redirect from="/" to="/" />
        </Switch>
      </div>
    </Router>
  )
}

Solution 8 - Javascript

So my work-around for this solution has been quite reliable, though it may be more manual of a solution than what you're looking to do.

The strategy that I've been using is to actually not even use the Link Component. Instead, you'll utilize the Tabs onChange property as a callback that can respond to Tab clicks, and track location manually with Props on the Parent.

You can import a utility called History from react-router that will allow you to manually push locations. While using React-Router, your component tree will have access to Location prop that has a pathname key with the string of your current location.

We will manually parse this string into the components that make up your current URL, then use a Switch statement to decide both which tab is currently selected and also where to link to when a tab is clicked. (This gives you a fair amount of control over navigation)

( e.g. ['', 'latest'] )

Here is a mock up of what your component MAY look like after integrating this solution.

import React from 'react';
import {History} from 'react-router';

function parseLocation(location) {
	if (String(location)) {
		var locationArray = location.split('/');
		return locationArray;
	} else {
		return false;
	}
};
function filterPath(path) {
    let locationArray = parseLocation(path);
    return locationArray[locationArray.length - 1];
};
var Navigation = React.createClass({
      mixins: [History],
      getPage() {
        if (this.props.location.pathname) {
		  let pathname = this.props.location.pathname;
		  let pageName = filterPath(pathname);
		  return pageName;
	    } else {
		  return false;
	    } 
      },
      decideContent() {
        let page = this.getPage();
        let content;
        switch(page) {
           case 'popular':
              content = 0;
           case 'latest':
              content = 1;
           case 'myideas':
              content = 2;
           default:
              content = 0;
        }
        return content;
      },
      handleTabChange(value) {
        let location = false;
        switch (value) {
           case 0:
             location = 'popular';
             break;
           case 1:
             location = 'latest';
             break;
           case 2:
             location = 'myideas';
             break;
        }
        if (location && location !== this.getPage()) {
          this.history.pushState(null, '/'+location);
        }
      },
      render() {
         var styles = {
          appBar: {
           flexWrap: 'wrap'
          },
          tabs: {
           width: '100%'
          }
         };
         let content = this.decideContent();
         let tabs =	<Tabs
				  onChange={this.handleTabChange}
				  value={content}
				>
				  <Tab label="Most Popular Ideas" value={0}  />
				  <Tab label="Latest Ideas" value={1}  />
				  <Tab label="My Ideas" value={2}  />
				</Tabs>;
        return (
         <AppBar showMenuIconButton={false} style={styles.appBar}>
           {tabs}
         </AppBar>
        );
      }
});

Solution 9 - Javascript

Check this link, I implemented the solution and worked for me

Composition in material UI

Solution 10 - Javascript

If you use NextJs, you can do it like that, and create your own component.

*i didn`t wrap the Tab with 'a' tag, because it added automatically

const WrapTab = (props) => {
const { href } = props
return (
<Link href={href} style={{ width: "100%" }}>
  <Tab {...props} />
</Link>
)}

and then this is your return

    <Tabs
      value={value}
      indicatorColor="primary"
      textColor="primary"
      onChange={handleChange}
      variant="fullWidth"
    >
        <WrapTab href="/testPage/?tab=0" icon={<MenuIcon />} />
        <WrapTab href="/testPage/?tab=1" icon={<StampIcon2 />} />
        <WrapTab href="/testPage/?tab=2" icon={<ShopIcon />} />
        <WrapTab href="/testPage/?tab=3" icon={<PenIcon />} />
        <WrapTab href="/testPage/?tab=4" icon={<ProfileIcon />} />
    </Tabs>

link to material-ui docs: https://material-ui.com/guides/composition/

Solution 11 - Javascript

For anyone looking to wrap Material-ui Link with Next.js Link

import Link from "next/link"
import MuiLink from "@material-ui/core/Link"

const CustomNextLink = ({href, alt}) => ({children, ...rest}) => (
<Link href={href} alt={alt}>
  <MuiLink {...rest}>
    {children}
  </MuiLink>
</Link>)

Then pass it to you Tab component

<Tab 
 key={...}
 label={title} 
 icon={icon} 
 component={CustomNextLink({href: to, alt: title})}
 style={...}
 className={...}
 classes={{selected: ...}}
 {...a11yProps(index)}
/>

Solution 12 - Javascript

Use the href="" option as shown below:

<Tab
 href="/en/getting-started"
 label="Contact US"
 style={{ color: "white", textDecoration: "none" }}
/>

To remove the ripple effect on clicking, use the option disableRipple

Solution 13 - Javascript

This seems to work for me

import { Link as RouterLink } from 'react-router-dom';
import Link from '@mui/material/Link';

<Link to={menuItem.url} component={RouterLink} aria-current="page">
{menuItem.label}
</Link>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionEvaldas BuinauskasView Question on Stackoverflow
Solution 1 - JavascripthazardousView Answer on Stackoverflow
Solution 2 - Javascripthigh incompetanceView Answer on Stackoverflow
Solution 3 - JavascriptSarath AkView Answer on Stackoverflow
Solution 4 - JavascriptKishan BView Answer on Stackoverflow
Solution 5 - JavascriptOgglasView Answer on Stackoverflow
Solution 6 - JavascriptYaroslav ZaklinskyView Answer on Stackoverflow
Solution 7 - JavascriptElia AhadiView Answer on Stackoverflow
Solution 8 - JavascriptMichael LyonsView Answer on Stackoverflow
Solution 9 - JavascriptJorge SantosView Answer on Stackoverflow
Solution 10 - JavascriptYehuda ZviView Answer on Stackoverflow
Solution 11 - JavascriptIsmailView Answer on Stackoverflow
Solution 12 - JavascriptHoritty TechMastersView Answer on Stackoverflow
Solution 13 - JavascriptatazminView Answer on Stackoverflow