Material-ui: [Tabs] How to sync with react-router

Created on 12 Dec 2019  路  8Comments  路  Source: mui-org/material-ui

My title is quite self-explanatory, I implemented a navigation bar using the <Tabs> which allows me to navigate through my website as one would expect. However, when/if I try to directly access an endpoint (e.g. /services or about), the <Tabs> are obviously out of sync and the <Tab> that should be set as active is not.

I am sure that this is a well-known issue with a "hacky" fix that should be included in the official doc, to avoid rookies like me posting this type of ticket. 馃榿

Tabs docs good to take

Most helpful comment

I created a react custom hook to handle this problem and keep the TabLink and url path in sync. Notice that the "to" prop of react-router Link is inherited from "value" prop of MUI Tab component if it's not provided.

TabLink.tsx

import React from 'react';
import { LinkProps } from 'react-router-dom';
import { Tab } from '@material-ui/core';
import { TabProps } from '@material-ui/core/Tab';
import RouterLink from './RouterLink';

const TabLink: React.FC<Omit<LinkProps, 'to'> & TabProps & { to?: LinkProps['to'] }> = ({
  to,
  value,
  ...props
}) => {
  return <Tab component={RouterLink} to={to ?? value} value={value} {...props} />;
};

export default TabLink;

RouterLink.tsx

import React from 'react';
import { Link, LinkProps } from 'react-router-dom';

const RouterLink: React.FC<Omit<LinkProps, 'innerRef' | 'ref'>> = (props, ref) => (
  <Link innerRef={ref} {...props} />
);

export default React.forwardRef(RouterLink);

useTabsWithRouter.ts

import { useRouteMatch } from 'react-router-dom';

export const useTabsWithRouter = (routes: string | string[], defaultRoute: string): string => {
  const match = useRouteMatch(routes);

  return match?.path ?? defaultRoute;
};

Example.tsx

const Example: React.FC = () => {
  const tabValue = useTabsWithRouter(['users/add', 'users/edit', 'users'], 'users');

  return (
    <div>
      <Tabs value={tabValue}>
        <TabLink value="users" label="Users" />
        <TabLink value="users/add" label="Users New" />
        <TabLink value="users/edit" label="Users Edit" />
      </Tabs>
    </div>
  );
};

export default Example;

IMPORTANT If you provide an array of routes (which is the normal use case) then you need to provide them in descendant order. This means that if you have nested routes like users, users/new, users/edit, etc. Then the order would be ['users/add', 'users/edit', 'users'].

All 8 comments

The integration should be already detailed in https://material-ui.com/guides/composition/#routing-libraries.

However, we might need to go into more details for the tabs.

You can find prior coverage in the history of the project:

I would propose the following diff:

diff --git a/docs/src/pages/guides/composition/composition.md b/docs/src/pages/guides/composition/composition.md
index 05d30b193..87c0a721b 100644
--- a/docs/src/pages/guides/composition/composition.md
+++ b/docs/src/pages/guides/composition/composition.md
@@ -149,6 +149,10 @@ It covers the Button, Link, and List components, you should be able to apply the

 {{"demo": "pages/guides/composition/ListRouter.js"}}

+### Tabs
+
+{{"demo": "pages/guides/composition/TabsRouter.js", "defaultCodeOpen": false, "bg": true}}
+
 ## Caveat with refs

 This section covers caveats when using a custom component as `children` or for the

diff --git a/docs/src/pages/guides/composition/TabsRouter.tsxb/docs/src/pages/guides/composition/TabsRouter.tsx
index 05d30b193..87c0a721b 100644
+import React from 'react';
+import Tabs from '@material-ui/core/Tabs';
+import Tab, { TabProps } from '@material-ui/core/Tab';
+import Paper from '@material-ui/core/Paper';
+import Typography from '@material-ui/core/Typography';
+import { Route, MemoryRouter } from 'react-router';
+import { Link as RouterLink, LinkProps as RouterLinkProps, useLocation } from 'react-router-dom';
+import { Omit } from '@material-ui/types';
+
+function TabLink(props: TabProps) {
+  const { value } = props;

+  const renderLink = React.useMemo(
+    () =>
+      React.forwardRef<HTMLAnchorElement, Omit<RouterLinkProps, 'innerRef' | 'to'>>(
+        (itemProps, ref) => (
+          // With react-router-dom@^6.0.0 use `ref` instead of `innerRef`
+          // See https://github.com/ReactTraining/react-router/issues/6056
+          <RouterLink to={value} {...itemProps} innerRef={ref} />
+        ),
+      ),
+    [value],
+  );

+  return <Tab component={renderLink} {...props} />;
+}
+
+function MyTabs() {
+  const location = useLocation();

+  return (
+    <Paper square>
+      <Tabs aria-label="tabs router example" value={location.pathname}>
+        <TabLink label="Inbox" value="/inbox" />
+        <TabLink label="Drafts" value="/drafts" />
+        <TabLink label="Trash" value="/trash" />
+      </Tabs>
+    </Paper>
+  );
+}
+
+export default function TabsRouter() {
+  return (
+    <div>
+      <MemoryRouter initialEntries={['/drafts']} initialIndex={0}>
+        <Route>
+          {({ location }) => (
+            <Typography gutterBottom>Current route: {location.pathname}</Typography>
+          )}
+        </Route>
+        <MyTabs />
+      </MemoryRouter>
+    </div>
+  );
+}

However, I couldn't make the TypeScript definition run without error. @eps1lon I suspect something is wrong between the expectation of a div and an element.

I created a react custom hook to handle this problem and keep the TabLink and url path in sync. Notice that the "to" prop of react-router Link is inherited from "value" prop of MUI Tab component if it's not provided.

TabLink.tsx

import React from 'react';
import { LinkProps } from 'react-router-dom';
import { Tab } from '@material-ui/core';
import { TabProps } from '@material-ui/core/Tab';
import RouterLink from './RouterLink';

const TabLink: React.FC<Omit<LinkProps, 'to'> & TabProps & { to?: LinkProps['to'] }> = ({
  to,
  value,
  ...props
}) => {
  return <Tab component={RouterLink} to={to ?? value} value={value} {...props} />;
};

export default TabLink;

RouterLink.tsx

import React from 'react';
import { Link, LinkProps } from 'react-router-dom';

const RouterLink: React.FC<Omit<LinkProps, 'innerRef' | 'ref'>> = (props, ref) => (
  <Link innerRef={ref} {...props} />
);

export default React.forwardRef(RouterLink);

useTabsWithRouter.ts

import { useRouteMatch } from 'react-router-dom';

export const useTabsWithRouter = (routes: string | string[], defaultRoute: string): string => {
  const match = useRouteMatch(routes);

  return match?.path ?? defaultRoute;
};

Example.tsx

const Example: React.FC = () => {
  const tabValue = useTabsWithRouter(['users/add', 'users/edit', 'users'], 'users');

  return (
    <div>
      <Tabs value={tabValue}>
        <TabLink value="users" label="Users" />
        <TabLink value="users/add" label="Users New" />
        <TabLink value="users/edit" label="Users Edit" />
      </Tabs>
    </div>
  );
};

export default Example;

IMPORTANT If you provide an array of routes (which is the normal use case) then you need to provide them in descendant order. This means that if you have nested routes like users, users/new, users/edit, etc. Then the order would be ['users/add', 'users/edit', 'users'].

What do you guys think about this version? In collaboration with @mbrevda: https://codesandbox.io/s/material-demo-ymnv8?file=/demo.js.

FWIW, I don't love the solution myself. Specifically, I'd prefer the declarative nature of using the <Tab> component and would rather avoid a (separate) array of paths in order to tell <Tabs/> which one is the active tab.

Perhaps if <Tabs value/> took a predicate passing each tab's props and expecting true or false we could cut down on some duplication/imperative code?

@mbrevda How would it look like?

psudocode:

const value = ({to}: Tab) => useRouteMatch(to))

Basically each tab gets passed to the function until it returns two. A la Array.find

Was this page helpful?
0 / 5 - 0 ratings

Related issues

reflog picture reflog  路  3Comments

FranBran picture FranBran  路  3Comments

newoga picture newoga  路  3Comments

mattmiddlesworth picture mattmiddlesworth  路  3Comments

mb-copart picture mb-copart  路  3Comments