React Material UI - Handle nav tabs when no children match the provided value

Viewed 1184

I have a NavBar in my app which uses the Material UI v5 - Nav tabs component with React Router v4. So when you navigate to something like /about-us, it will read that from the url and then change to that tab.

The problem

I have child pages, such as /about-us/john, /about-us/deborah but when I navigate to those pages, I get an error like this:

MUI: The value provided to the Tabs component is invalid.

None of the Tabs' children match with "/about-us/john".

You can provide one of the following values: /, /about-us.

You'll see in the sample code section below my current approach, I'm thinking if there's a way to have a list of "valid" tab URLs but then am unsure of how to handle when there are child pages as described above.

The desired outcome

When I navigate to a URL like /about-us/john, it:

  • Won't throw the above error.
  • [Nice to have] Still highlight the About Us tab.

Sample code

// NavBar.tsx
import { Link, useLocation } from 'react-router-dom';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';

export const NavBar = () => {
  const location = useLocation();
  const currentTab = location.pathname;

  return (
    <Tabs value={currentTab} >
      <Tab label="Home" value="/" to="/" component={Link} />
      <Tab label="About Us" value="/about-us" to="/about-us" component={Link} />
    </Tabs>
  );
}
// App.tsx
import { BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import NavBar from './NavBar';
import Home from './Home';
import AboutUs from './AboutUs';
import AboutUsDetail from './AboutUsDetail';

export const App = () => {
  return (
    <Router>
      <NavBar />
      <Switch>
        <Route exact path="/">
          <Home />
        </Route>
        <Route exact path="/about-us">
          <AboutUs />
        </Route>
        <Route exact path="/about-us/:name">
          // Uses `useParams` from react-router-dom to extract the name.
          <AboutUsDetail />
        </Route>
      </Switch>
    </Router>
  );
};
2 Answers

Issue

When matching and rendering a more deeply "nested" path, i.e. "/about-us/john" there is no specified matching Tab component value prop to render the tab correctly.

Solution

Taking your sample NavBar you effectively need only match against the "root" path segment of any route, in this case, either "/" or "/about-us". There are likely several ways to get this, i.e. string splitting, REGEX, etc... The following example uses a REGEX with a capturing group to grab the first segment. /^(\/[^\/]*)/g matches from the start of the string matching a "/" and includes all characters until the end of the string or the next "/" character.

console.log([..."/".matchAll(/^(\/[^/]*)/g)][0][1]);
console.log([..."/about-us".matchAll(/^(\/[^/]*)/g)][0][1]);
console.log([..."/about-us/john".matchAll(/^(\/[^/]*)/g)][0][1]);

const NavBar = () => {
  const location = useLocation();

  const [[, currentRoot]] = location.pathname.matchAll(/^(\/[^/]*)/g);

  return (
    <Tabs value={currentRoot}>
      <Tab label="Home" value="/" to="/" component={Link} />
      <Tab label="About Us" value="/about-us" to="/about-us" component={Link} />
    </Tabs>
  );
};

Edit react-material-ui-handle-nav-tabs-when-no-children-match-the-provided-value

enter image description here

Just an unrelated side-note regarding rendering routes into the Switch, you can avoid the unnecessary use of the exact prop by properly ordering the routes from more specific paths to less specific paths. If no higher, more specific route above matches then matching continues until the least specific match is found and rendered.

<Switch>
  <Route path="/about-us/:name">
    <AboutUsDetail />
  </Route>
  <Route path="/about-us">
    <AboutUs />
  </Route>
  <Route path="/">
    <Home />
  </Route>
</Switch>

The Tab component takes an exact value and not a path like what Route does, so the dynamic part :name in /about-us/:name is not understandable by Tabs.

I have a workaround in case you want the error you mentioned to disappear from the console. you can add another invisible Tab that would hold a value of the current pathname, so your Tabs will always have a match.

First, add the following CSS class for hiding the extra Tab to your app.css file or any file, and make sure to import it

.hidden {
    visibility: hidden;
    position: absolute;
    left: -1000px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

Then, add the extra Tab

import { Link, useLocation } from 'react-router-dom';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';

const NavBar = () => {
    const location = useLocation();
    const currentTab = location.pathname;
    return (
        <Tabs value={currentTab} >
            <Tab label="Home" value="/" to="/" component={Link} />
            <Tab label="About Us" value='/about-us' to='/about-us' component={Link} />
            <Tab className="hidden" label="*" value={currentTab} to={currentTab} component={Link} />
        </Tabs>
    );
}

export default NavBar

I'm using the following versions of libs

"react": "^17.0.2"
"react-dom": "^17.0.2"
"@mui/material": "^5.2.1"
"react-router-dom": "^5.2.0"

Related