Material-UI's Tabs integration with react router 4?

ReactjsMaterial UiReact Router-V4

Reactjs Problem Overview


The new react-router syntax uses the Link component to move around the routes. But how could this be integrated with material-ui?

In my case, I'm using tabs as the main navigation system, So in theory I should have something like this:

const TabLink = ({ onClick, href, isActive, label }) => 
  <Tab
    label={label}
    onActive={onClick}
  />



export default class NavBar extends React.Component {
  render () {
    return (
      <Tabs>
        <Link to="/">{params => <TabLink label="Home" {...params}/>}</Link>
        <Link to="/shop">{params => <TabLink label="shop" {...params}/>}</Link>
        <Link to="/gallery">{params => <TabLink label="gallery" {...params}/>}</Link>
      </Tabs>
    )
  }
}

But when it renders, material-ui throws an error that the child of Tabs must be a Tab component. What could be the way to proceed? How do I manage the isActive prop for the tab?

Thanks in advance

Reactjs Solutions


Solution 1 - Reactjs

Another solution (https://codesandbox.io/s/l4yo482pll) with no handlers nor HOCs, just pure react-router and material-ui components:

import React, { Fragment } from "react";
import ReactDOM from "react-dom";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import { Switch, Route, Link, BrowserRouter, Redirect } from "react-router-dom";
    
function App() {
  const allTabs = ['/', '/tab2', '/tab3'];

  return (
    <BrowserRouter>
      <div className="App">
        <Route
          path="/"
          render={({ location }) => (
            <Fragment>
              <Tabs value={location.pathname}>
                <Tab label="Item One" value="/" component={Link} to={allTabs[0]} />
                <Tab label="Item Two" value="/tab2" component={Link} to={allTabs[1]} />
                <Tab
                  value="/tab3"
                  label="Item Three"
                  component={Link}
                  to={allTabs[2]}
                />
              </Tabs>
              <Switch>
                <Route path={allTabs[1]} render={() => <div>Tab 2</div>} />
                <Route path={allTabs[2]} render={() => <div>Tab 3</div>} />
                <Route path={allTabs[0]} render={() => <div>Tab 1</div>} />
              </Switch>
            </Fragment>
          )}
        />
      </div>
    </BrowserRouter>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Solution 2 - Reactjs

My instructor helped me with using React Router 4.0's withRouter to wrap the Tabs component to enable history methods like so:

import React, {Component} from "react";
import {Tabs, Tab} from 'material-ui';
import { withRouter } from "react-router-dom";

import Home from "./Home";
import Portfolio from "./Portfolio";

class NavTabs extends Component {

 handleCallToRouter = (value) => {
   this.props.history.push(value);
 }

  render () {
     return (
      <Tabs
        value={this.props.history.location.pathname}
        onChange={this.handleCallToRouter}
        >
        <Tab
          label="Home"
          value="/"
        >
        <div>
           <Home />
        </div>
        </Tab>
        <Tab
          label="Portfolio"
          value="/portfolio"
            >
          <div>
            <Portfolio />
          </div>
        </Tab>
      </Tabs>           
    )
  }
}

export default withRouter(NavTabs)  

Simply add BrowserRouter to index.js and you're good to go.

Solution 3 - Reactjs

The error you are seeing from material-ui is because it expects to have a <Tab> component rendered as direct child of <Tabs> component.

Now, here is a way that I've found to integrate the link into the <Tabs> component without loosing the styles:

import React, {Component} from 'react';
import {Tabs, Tab} from 'material-ui/Tabs';
import {Link} from 'react-router-dom';

export default class MyComponent extends Component {
    render() {
        const {location} = this.props;
        const {pathname} = location;

        return (
            <Tabs value={pathname}>
                <Tab label="First tab" containerElement={<Link to="/my-firs-tab-view" />} value="/my-firs-tab-view">
                    {/* insert your component to be rendered inside the tab here */}
                </Tab>
                <Tab label="Second tab" containerElement={<Link to="/my-second-tab-view" />} value="/my-second-tab-view">
                    {/* insert your component to be rendered inside the tab here */}
                </Tab>
            </Tabs>
        );
    }
}

To manage the 'active' property for the tabs, you can use the value property in the <Tabs> component and you also need to have a value property for each tab, so when both of the properties match, it will apply the active style to that tab.

Solution 4 - Reactjs

Here's another solution, using the beta of Material 1.0 and adding browser Back/Forward to the mix:

import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from 'material-ui/styles';
import AppBar from 'material-ui/AppBar';
import Tabs, { Tab } from 'material-ui/Tabs';
import { withRouter } from "react-router-dom";
import Home from "./Home";
import Portfolio from "./Portfolio";

function TabContainer(props) {
  return <div style={{ padding: 20 }}>{props.children}</div>;
}

const styles = theme => ({
  root: {
    flexGrow: 1,
    width: '100%',
    marginTop: theme.spacing.unit * 3,
    backgroundColor: theme.palette.background.paper,
  },
});

class NavTabs extends React.Component {
  state = {
    value: "/",
  };

  componentDidMount() {
    window.onpopstate = ()=> {
      this.setState({
        value: this.props.history.location.pathname
      });
  }
}

  handleChange = (event, value) => {
    this.setState({ value });
    this.props.history.push(value);
  };
  
  render() {
    const { classes } = this.props;
    const { value } = this.state;

    return (
      <div className={classes.root}>
        <AppBar position="static" color="default">
          <Tabs
            value={value}
            onChange={this.handleChange}
            scrollable
            scrollButtons="on"
            indicatorColor="primary"
            textColor="primary"
          >
            <Tab label="Home" value = "/" />
            <Tab label="Portfolio" value = "/portfolio"/>
          </Tabs>
        </AppBar>
        {value === "/" && <TabContainer>{<Home />}</TabContainer>}
        {value === "/portfolio" && <TabContainer>{<Portfolio />}</TabContainer>}
      </div>
    );
  }
}

NavTabs.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withRouter(withStyles(styles)(NavTabs));

Solution 5 - Reactjs

Solution with Tab highlight, Typescript based and works well with react-route v5:
Explanation: <Tab/> here work as a link to React router. Values in <Tab/> to={'/all-event'} and value={'/all-event'} should be same in order to highlgiht

import { Container, makeStyles, Tab, Tabs } from '@material-ui/core';
import React from 'react';
import {
  Link,
  Route,
  Switch,
  useLocation,
  Redirect,
} from 'react-router-dom';
import AllEvents from './components/AllEvents';
import UserEventsDataTable from './components/UserEventsDataTable';

const useStyles = makeStyles(() => ({
  container: {
    display: 'flex',
    justifyContent: 'center',
  },
}));

function App() {
  const classes = useStyles();
  const location = useLocation();

  return (
    <>
      <Container className={classes.container}>
        <Tabs value={location.pathname}>
          <Tab
            label='All Event'
            component={Link}
            to={`/all-event`}
            value={`/all-event`}
          />
          <Tab
            label='User Event'
            component={Link}
            to={`/user-event`}
            value={`/user-event`}
          />
        </Tabs>

      </Container>
      <Switch>
        <Route path={`/all-event`}>
          <AllEvents />
        </Route>
        <Route path={`/user-event`}>
          <UserEventsDataTable />
        </Route>
        <Route path={`/`}>
          <Redirect from='/' to='/all-event' />
        </Route>
      </Switch>
    </>
  );
}

export default App;

Solution 6 - Reactjs

You can use browserHistory instead of React-Router Link component

import { browserHistory } from 'react-router'

// Go to /some/path.
onClick(label) {
  browserHistory.push('/${label}');
}

// Example for Go back
//browserHistory.goBack()

<Tabs>
  <Tab
    label={label}
    onActive={() => onClick(label)}
  />
</Tabs>

As you see you can simply push() your target to the browserHistory

Solution 7 - Reactjs

As @gkatchmar says you can use withRouter high-order component but you can also use context API. Since @gkatchmar showed withRouter already I will only show context API. Bear in mind that this is an experimental API.

https://stackoverflow.com/a/42716055/3850405

import React, {Component} from "react";
import {Tabs, Tab} from 'material-ui';
import * as PropTypes from "prop-types";

export class NavTabs extends Component {
constructor(props) {
 super(props);
}

static contextTypes = {
    router: PropTypes.object
}

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

  render () {
     return (
      <Tabs
        value={this.context.router.history.location.pathname}
        onChange={this.handleChange}
        >
        <Tab
          label="Home"
          value="/"
        >
        <div>
           <Home />
        </div>
        </Tab>
        <Tab
          label="Portfolio"
          value="/portfolio"
            >
          <div>
            <Portfolio />
          </div>
        </Tab>
      </Tabs>           
    )
  }
}

Solution 8 - Reactjs

Here's a simple solution using the useLocation hook. No state needed. React router v5 though.

import { Tab, Tabs } from '@material-ui/core';
import { matchPath, NavLink, useLocation } from 'react-router-dom';

const navItems = [
  {
    id: 'one',
    path: '/one',
    text: 'One',
  },
  {
    id: 'two',
    path: '/two',
    text: 'Two',
  },
  {
    id: 'three',
    path: '/three',
    text: 'Three',
  },
];

export default function Navigation() {
  const { pathname } = useLocation();
  const activeItem = navItems.find((item) => !!matchPath(pathname, { path: item.path }));
  return (
    <Tabs value={activeItem?.id}>
      {navItems.map((item) => (
        <Tab key={item.id} value={item.id} label={item.text} component={NavLink} to={item.path} />
      ))}
    </Tabs>
  );
}

Solution 9 - Reactjs

 <BrowserRouter>
<div className={classes.root}>
  <AppBar position="static" color="default">
    <Tabs
      value={this.state.value}
      onChange={this.handleChange}
      indicatorColor="primary"
      textColor="primary"
      fullWidth
    >
      <Tab label="Item One" component={Link} to="/one" />
      <Tab label="Item Two" component={Link} to="/two" />
    </Tabs>
  </AppBar>

  <Switch>
    <Route path="/one" component={PageShell(ItemOne)} />
    <Route path="/two" component={PageShell(ItemTwo)} />
  </Switch>
</div>

Solution 10 - Reactjs

I've created this hook to help control the tabs and generate the default value that catches from the location URL.

const useTabValue = (array, mainPath = "/") => {
  const history = useHistory();
  const { pathname } = useLocation();
  const [value, setValue] = useState(0);
  const pathArray = pathname.split("/");

  function handleChange(_, nextEvent) {
    setValue(nextEvent);
    history.push(`${mainPath}/${array[nextEvent]}`);
  }

  const findDefaultValue = useCallback(() => {
    return array.forEach((el) => {
      if (pathArray.indexOf(el) > 0) {
        setValue(array.indexOf(el));
        return;
      }
    });
  }, [pathArray, array]);

  useEffect(() => {
    findDefaultValue();
  }, [findDefaultValue]);
  return {
    handleChange,
    value,
  };
};

then I have used it like this :

const NavigationBar = () => {
  const classes = useStyles();
  const allTabs = useMemo(() => ["home", "search"]);
  const { handleChange, value } = useTabValue(allTabs, "/dashboard");
  return (
    <div className={classes.navBarContainer}>
      <Tabs
        centered
        value={value}
        variant="fullWidth"
        onChange={handleChange}
        className={classes.navBar}
      >
        <Tab color="textPrimary" icon={<HomeIcon />} />
        <Tab color="textPrimary" icon={<ExploreIcon />} />
      </Tabs>
    </div>
  );
};

Solution 11 - Reactjs

I solved this in a much easier fashion (I was surprised this worked so well - maybe there's a problem I haven't found out). I'm using Router 6 and React 17 (I know these packages are newer). In any case, I just used the useNavigate hook in the handleChange function. Thus, now there is NO need for Switch and the code becomes much simpler. See below:

let navigate = useNavigate();
const [selection, setSelection] = useState();

const handleChange = (event, newValue) => {
    setSelection(newValue);
    navigate(`${newValue}`);
}

return (
    <Tabs value={selection} onChange={handleChange}>
         <Tab label="Products" value="products"  />
         <Tab label="Customers" value="customers" />
         <Tab label="Invoices" value="invoices" />
    </Tabs>
  );
}

The handleChange function updates 'selection' which controls the display of the tabs, and also navigates to the right path. if you set the component somewhere in your React space, and set correctly a :style route (as explained by React Router: https://reactrouter.com/docs/en/v6/getting-started/overview), you can also control in which area of the page will the content be rendered. Hope it helps somebody!

Solution 12 - Reactjs

I got it working this way in my app:

import React, {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import {makeStyles} from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';
import Container from "@material-ui/core/Container";
import {Link} from "react-router-dom";
import MenuIcon from "@material-ui/icons/Menu";
import VideoCallIcon from "@material-ui/icons/VideoCall";

const docStyles = makeStyles(theme => ({
    root: {
        display: 'flex',
        '& > * + *': {
            marginLeft: theme.spacing(2),
        },
    },
    appBarRoot: {
        flexGrow: 1,
    },
    headline: {
        marginTop: theme.spacing(2),
    },
    bodyCopy: {
        marginTop: theme.spacing(1),
        fontSize: '1.2rem',
    },
    tabContents: {
        margin: theme.spacing(3),
    },
}));

function TabPanel(props) {
    const {children, value, index, classes, ...other} = props;

    return (
        <div
            role="tabpanel"
            hidden={value !== index}
            id={`simple-tabpanel-${index}`}
            aria-labelledby={`simple-tab-${index}`}
            {...other}
        >
            {value === index && (
                <Container>
                    <Box className={classes.tabContents}>
                        {children}
                    </Box>
                </Container>
            )}
        </div>
    );
}

function a11yProps(index) {
    return {
        id: `simple-tab-${index}`,
        'aria-controls': `simple-tabpanel-${index}`,
    };
}

function TabOneContents(props) {
    const {classes} = props;
    return (
        <>
            <Typography variant="h4" component={'h1'} className={classes.headline}>
                Headline 1
            </Typography>

            <Typography variant="body1" className={classes.bodyCopy}>
                Body Copy 1
            </Typography>
        </>
    )
}

function TabTwoContents(props) {
    const {classes} = props;
    const nurseOnboardingPath = '/navigator/onboarding/' + Meteor.userId() + '/1';

    return (
        <>
            <Typography variant="h4" component={'h1'} className={classes.headline}>
                Headline 2
            </Typography>

            <Typography variant="body1" className={classes.bodyCopy}>
                Body Copy 2
            </Typography>
        </>
    )
}

export default function MUITabPlusReactRouterDemo(props) {
    const {history, match} = props;
    const propsForDynamicClasses = {};
    const classes = docStyles(propsForDynamicClasses);
    const [value, setValue] = React.useState(history.location.pathname.includes('/tab_2') ? 1 : 0);

    const handleChange = (event, newValue) => {
        setValue(newValue);
        const pathName = '/' + (value == 0 ? 'tab_1' : 'tab_2');
        history.push(pathName);
    };


    return (
        <div className={classes.appBarRoot}>
            <AppBar position="static" color="transparent">
                <Tabs value={value} onChange={handleChange} aria-label="How It Works" textColor="primary">
                    <Tab label="Tab 1" {...a11yProps(0)} />
                    <Tab label="Tab 2" {...a11yProps(1)} />
                </Tabs>
            </AppBar>
            <TabPanel value={value} index={0} classes={classes}>
                <TabOneContents classes={classes}/>
            </TabPanel>
            <TabPanel value={value} index={1} classes={classes}>
                <TabTwoContents classes={classes}/>
            </TabPanel>
        </div>
    );
}

...and in React Router:

[.....]
<Route exact path="/tab_1"
	   render={(routeProps) =>
		   <MUITabPlusReactRouterDemo history={routeProps.history}
		   />
	   }/>

<Route exact path="/tab_2"
	   render={(routeProps) =>
		   <MUITabPlusReactRouterDemo history={routeProps.history}                           />
	   }/>
[.....]

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
QuestionDaniel RamosView Question on Stackoverflow
Solution 1 - ReactjsGildas GarciaView Answer on Stackoverflow
Solution 2 - ReactjsgkatchmarView Answer on Stackoverflow
Solution 3 - ReactjserickpenicheView Answer on Stackoverflow
Solution 4 - ReactjsgkatchmarView Answer on Stackoverflow
Solution 5 - ReactjsJasurbek NabijonovView Answer on Stackoverflow
Solution 6 - ReactjsPiotr OView Answer on Stackoverflow
Solution 7 - ReactjsOgglasView Answer on Stackoverflow
Solution 8 - ReactjsralfstxView Answer on Stackoverflow
Solution 9 - ReactjsFritzView Answer on Stackoverflow
Solution 10 - ReactjsChadjaa SofianneView Answer on Stackoverflow
Solution 11 - ReactjsHashiView Answer on Stackoverflow
Solution 12 - ReactjsVikRView Answer on Stackoverflow