I'm new to React and made the code below
Where I'm trying to insert a TabPanel but it's getting an error
Uncaught TypeError: No TabContext provided at TabPanel (TabPanel.js:26)
at renderWithHooks (react-dom.development.js:14803)
How do I adjust this error?
import React, {useState, useEffect} from "react";
import TextField from '#material-ui/core/TextField';
import Button from '#material-ui/core/Button';
import MenuItem from '#material-ui/core/MenuItem';
import Fetch from "../../../services/Fetch";
import InputDatePicker from "../../../components/InputDatepicker";
import CurrencyTextField from '#unicef/material-ui-currency-textfield';
import bridge from "./bridge";
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import TabPanel from '#material-ui/lab/TabPanel';
export default (props)=>{
const [state, setState] = useState(bridge.getForm(props.idUsidUser));
const [value, setValue] = useState(0);
useEffect(()=>{
setTimeout(()=>{
var elements = document.querySelectorAll("div[min='0']");
elements.forEach((element) => {
if(element.children.length > 0){
element.children[0].min = 0;
}
});
}, 300);
}, []);
bridge.setForm = setState;
const closeModal = (event) =>{
document.querySelector("#form").removeAttribute("style");
document.querySelector("html").removeAttribute("style");
document.querySelector(".modal-overlay").removeAttribute("style");
}
const onInputChange = (event) => {
setState({
...state,
[event.target.name]: event.target.value
});
}
const handleChange = (event, newValue) => {
setValue(newValue);
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
return (
<form onSubmit={onSubmit}>
<h3 className="featured">Create</h3>
<Tabs value={value} onChange={handleChange} aria-label="simple tabs example">
<Tab label="Tab 1" {...a11yProps(0)} />
<Tab label="Tab 2" {...a11yProps(1)} />
<Tab label="Tab 3" {...a11yProps(2)} />
<Tab label="Tab 4" {...a11yProps(3)} />
<Tab label="Tab 5" {...a11yProps(4)} />
</Tabs>
<TabPanel value={value} index={0}>
Item One
</TabPanel>
</form>
);
}
I don't know what I did wrong for React to acknowledge the error.
What to do in this case? Well, I followed the basic principles that I studied in the framework documentation
Comments were a little confusing for me, so here's a clear answer for others who run into this:
The error comes when using the Material UI Basic Tabs demo. They use a 'TabPanel' component in this, but as defined by their function as shown in the full code of their example (see snippet below)
The other type of TabPanel is part of the MUI labs and is imported from MUI directly:
import TabPanel from '#mui/lab/TabPanel';
But that won't work with the code in their demo example.
Demo link: https://mui.com/material-ui/react-tabs/
So if you are using the demo on the page quoted, you also need to use the same function they use:
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
I have a set of tabs that I want to be followed by an add button (to add a new tab). I just added a button to the children of the Tabs component and it renders and works exactly how I want it to, but there are so many warnings in the developer console.
<AppBar position="static">
<Tabs
value={this.props.selectedTab}
onChange={this.handleTabSelect}
textColor="secondary"
>
{this.props.ListOfStuff.map(el => {
return (
<Tab
className={classes.tabButton}
value={el.ClientId}
label={el.Label}
id={el.ClientId}
aria-controls={"tabPanel-" + el.ClientId}
key={"tab-" + el.ClientId}
/>
);
})}
<Button
className={classes.addButton}
onClick={this.addNewTab}
>
<AddIcon color="secondary" className={classes.addIcon} />
NEW TAB
</Button>
</Tabs>
</AppBar>
warnings like:
- Warning: Received false for a non-boolean attribute indicator.
- Warning: React does not recognize the textColor prop on a DOM element.
- Warning: ForwardRef(InputBase) contains an input of type time with both value and defaultValue props.
Any suggestions on how to keep it rendering in the same way but to get rid of all of these warnings?
Thanks
The Tabs component clones its child elements (presumed to be Tab elements) in order to pass additional properties (e.g. properties related to the "selected" tab).
The warnings are caused by these additional properties being passed to the Button component. You can fix these warnings by wrapping Button in a component that ignores the additional properties passed by Tabs such as the following:
const ButtonInTabs = ({ className, onClick, children }) => {
return <Button className={className} onClick={onClick} children={children} />;
};
Full working example:
import React 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 Button from "#material-ui/core/Button";
import AddIcon from "#material-ui/icons/Add";
function TabPanel(props) {
const { children, value, index, ...other } = props;
return (
<Typography
component="div"
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
<Box p={3}>{children}</Box>
</Typography>
);
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.any.isRequired,
value: PropTypes.any.isRequired
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
},
addButton: {
color: "white"
}
}));
const ButtonInTabs = ({ className, onClick, children }) => {
return <Button className={className} onClick={onClick} children={children} />;
};
export default function SimpleTabs() {
const classes = useStyles();
const [value, setValue] = React.useState(0);
const [showThirdTab, setShowThirdTab] = React.useState(false);
const handleChange = (event, newValue) => {
setValue(newValue);
};
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs
value={value}
onChange={handleChange}
aria-label="simple tabs example"
>
<Tab label="Item One" {...a11yProps(0)} />
<Tab label="Item Two" {...a11yProps(1)} />
{showThirdTab && <Tab label="Item Three" {...a11yProps(2)} />}
<ButtonInTabs
onClick={() => setShowThirdTab(true)}
className={classes.addButton}
>
<AddIcon color="secondary" />
New Tab
</ButtonInTabs>
</Tabs>
</AppBar>
<TabPanel value={value} index={0}>
Item One
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
</div>
);
}
I'm trying to implement Sub-menu (nested menu).
It's worth to mention that I'm using hydra component and don't have previous experience with redux (started learning it a few days ago because of this specific problem).
I've followed the example provided on material-ui for nested list https://material-ui.com/demos/lists/#nested-list. And tutorial from
https://marmelab.com/react-admin/Theming.html#using-a-custom-menu for custom menu implementation.
So I have a few questions.
1) Can I have stateful component (MyMenu) just for handling toggling of menu items?
An example is not related to react-admin but its just example what I mean.
import React, { Component } from "react";
import { connect } from "react-redux";
import { addArticle } from "../actions/index";
const mapDispatchToProps = dispatch => {
return {
addArticle: article => dispatch(addArticle(article))
};
};
class ConnectedForm extends Component {
constructor() {
super();
this.state = {
title: ""
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({ [event.target.id]: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
const { title } = this.state;
const id = uuidv1();
this.props.addArticle({ title, id });
this.setState({ title: "" });
}
render() {
const { title } = this.state;
return (
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="title">Title</label>
<input
type="text"
className="form-control"
id="title"
value={title}
onChange={this.handleChange}
/>
</div>
<button type="submit" className="btn btn-success btn-lg">
SAVE
</button>
</form>
);
}
}
const Form = connect(null, mapDispatchToProps)(ConnectedForm);
export default Form;
2) If not, can I achieve that by declaring a new state in store, for example, open: false, and then using the custom reducer to handle that.
3(bonus). If it's not a problem I would appreciate if someone can put me in the right direction which things to start learning first so I can less painfully manage to solve issues related to this amazing framework :)
The react-admin demo now shows a way to do so in examples/demo/src/layout/Menu.js:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import compose from 'recompose/compose';
import SettingsIcon from '#material-ui/icons/Settings';
import LabelIcon from '#material-ui/icons/Label';
import { withRouter } from 'react-router-dom';
import {
translate,
DashboardMenuItem,
MenuItemLink,
Responsive,
} from 'react-admin';
import visitors from '../visitors';
import orders from '../orders';
import invoices from '../invoices';
import products from '../products';
import categories from '../categories';
import reviews from '../reviews';
import SubMenu from './SubMenu';
class Menu extends Component {
state = {
menuCatalog: false,
menuSales: false,
menuCustomers: false,
};
static propTypes = {
onMenuClick: PropTypes.func,
logout: PropTypes.object,
};
handleToggle = menu => {
this.setState(state => ({ [menu]: !state[menu] }));
};
render() {
const { onMenuClick, open, logout, translate } = this.props;
return (
<div>
{' '}
<DashboardMenuItem onClick={onMenuClick} />
<SubMenu
handleToggle={() => this.handleToggle('menuSales')}
isOpen={this.state.menuSales}
sidebarIsOpen={open}
name="pos.menu.sales"
icon={<orders.icon />}
>
<MenuItemLink
to={`/commands`}
primaryText={translate(`resources.commands.name`, {
smart_count: 2,
})}
leftIcon={<orders.icon />}
onClick={onMenuClick}
/>
<MenuItemLink
to={`/invoices`}
primaryText={translate(`resources.invoices.name`, {
smart_count: 2,
})}
leftIcon={<invoices.icon />}
onClick={onMenuClick}
/>
</SubMenu>
<SubMenu
handleToggle={() => this.handleToggle('menuCatalog')}
isOpen={this.state.menuCatalog}
sidebarIsOpen={open}
name="pos.menu.catalog"
icon={<products.icon />}
>
<MenuItemLink
to={`/products`}
primaryText={translate(`resources.products.name`, {
smart_count: 2,
})}
leftIcon={<products.icon />}
onClick={onMenuClick}
/>
<MenuItemLink
to={`/categories`}
primaryText={translate(`resources.categories.name`, {
smart_count: 2,
})}
leftIcon={<categories.icon />}
onClick={onMenuClick}
/>
</SubMenu>
<SubMenu
handleToggle={() => this.handleToggle('menuCustomer')}
isOpen={this.state.menuCustomer}
sidebarIsOpen={open}
name="pos.menu.customers"
icon={<visitors.icon />}
>
<MenuItemLink
to={`/customers`}
primaryText={translate(`resources.customers.name`, {
smart_count: 2,
})}
leftIcon={<visitors.icon />}
onClick={onMenuClick}
/>
<MenuItemLink
to={`/segments`}
primaryText={translate(`resources.segments.name`, {
smart_count: 2,
})}
leftIcon={<LabelIcon />}
onClick={onMenuClick}
/>
</SubMenu>
<MenuItemLink
to={`/reviews`}
primaryText={translate(`resources.reviews.name`, {
smart_count: 2,
})}
leftIcon={<reviews.icon />}
onClick={onMenuClick}
/>
<Responsive
xsmall={
<MenuItemLink
to="/configuration"
primaryText={translate('pos.configuration')}
leftIcon={<SettingsIcon />}
onClick={onMenuClick}
/>
}
medium={null}
/>
<Responsive
small={logout}
medium={null} // Pass null to render nothing on larger devices
/>
</div>
);
}
}
const mapStateToProps = state => ({
open: state.admin.ui.sidebarOpen,
theme: state.theme,
locale: state.i18n.locale,
});
const enhance = compose(
withRouter,
connect(
mapStateToProps,
{}
),
translate
);
export default enhance(Menu);
I've searched the same question. But couldn't find a nested menu implementation. So I wrote my own. Check the code below;
import React, { Component, createElement } from "react";
import {
Admin,
Resource,
Layout,
MenuItemLink,
getResources
} from "react-admin";
import jsonServerProvider from "ra-data-json-server";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import {
List,
ListItem,
Collapse,
ListItemText,
ListItemIcon
} from "#material-ui/core";
import { ExpandLess, ExpandMore, StarBorder, LabelIcon } from "#material-ui/icons";
import { withStyles } from "#material-ui/core/styles";
const menuStyles = theme => ({
nested: {
paddingLeft: theme.spacing.unit * 4
}
});
class Menu extends Component {
menuList = [
{ name: "A", label: "Top menu 1", icon: <LabelIcon /> },
{ name: "B", label: "Top menu 2", icon: <LabelIcon /> },
{ name: "c", label: "Top menu 3", icon: <LabelIcon /> }
];
constructor(props) {
super(props);
this.state = { open: "A" };
}
render() {
const { resources, onMenuClick, logout } = this.props;
return (
<div>
<List component="nav">
{this.menuList.map(menu => {
return (
<div key={menu.name}>
<ListItem
button
onClick={() => this.setState(state => ({ open: menu.name }))}
>
<ListItemIcon>{menu.icon}</ListItemIcon>
<ListItemText inset primary={menu.label} />
{this.state.open == menu.name ? (
<ExpandLess />
) : (
<ExpandMore />
)}
</ListItem>
<Collapse
in={this.state.open == menu.name}
timeout="auto"
unmountOnExit
>
<List component="div" disablePadding>
{resources
.filter(x => x.options.menu == menu.name)
.map((resource, i) => (
<MenuItemLink
key={"m" + i}
to={`/${resource.name}`}
primaryText={resource.options.label || resource.name}
leftIcon={
resource.icon
? createElement(resource.icon)
: undefined
}
onClick={onMenuClick}
className={this.props.classes.nested}
/>
))}
</List>
</Collapse>
</div>
);
})}
</List>
</div>
);
}
}
var MenuWithStyles = withStyles(menuStyles)(Menu);
const MyMenu = withRouter(
connect(state => ({
resources: getResources(state)
}))(MenuWithStyles)
);
const MyLayout = props => <Layout {...props} menu={MyMenu} />;
const App = () => (
<Admin
...
appLayout={MyLayout}
>
<Resource
...
options={{ label: 'Page 1' menu: "A" }}
/>
<Resource
...
options={{ label: 'Page 2' menu: "A" }}
/>
<Resource
...
options={{ label: 'Page 3' menu: "B" }}
/>
</Admin>
);
I need to build a drop down menu in a modal popup window with using Material UI
I have drop down menu in my modal window, and as a value I see the last item is "Third" and when I want to select for instance "First" it doesn't work, the menu doesn't select it and still having the last one item as a value in menu
I have 2 files App.js and list.js
App.js code:
import React, { Component } from 'react';
import './App.css';
import {AppButtons} from './components/button'
import {AppList} from './components/list'
import Dialog from 'material-ui/Dialog'
import FlatButton from 'material-ui/FlatButton'
export default class App extends Component {
constructor (props) {
super (props)
this.state = {
isModalOpen: false,
itemsList: [
{name: 'First'},
{name: 'Second'},
{name: 'Third'}
],
}
this.handleChange = this.handleChange.bind(this)
}
handleChange = () => this.setState({this.state.itemsList});
render() {
const actions = [
<FlatButton
label="Save"
primary={true}
onClick={() => this.setState({isModalOpen: false})}
/>,
<FlatButton
label="Cancel"
primary={true}
onClick={() => this.setState({isModalOpen: false})}
/>,
];
return (
<div className="container">
<AppButtons
openModal = {() => this.setState ({isModalOpen: true})}
/>
<Dialog
title="Numbers structure"
actions={actions}
open={this.state.isModalOpen}
onRequestClose={() => this.setState({isModalOpen: true})}
>
<AppList
items = {this.state.itemsList}
/>
</Dialog>
</div>
);
}
}
And list.js code:
import React from 'react'
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
const styles = {
customWidth: {
width: 200,
},
};
export const AppList = (props) => {
return (
<div>
<DropDownMenu
style={styles.customWidth}
onChange={this.handleChange}
>
{props.items.map((item, key) => {
return (
<MenuItem
primaryText = {item.name}
openImmediately={true}
autoWidth={false}/>
)
})
}
</DropDownMenu>
<br />
</div>
)
}
Here is the shot
It's because you aren't passing down your handleChange function as a prop to <AppList/>:
<AppList
items = {this.state.itemsList}
/>
Change it to:
<AppList
items = {this.state.itemsList}
handleChange={this.handleChange}
/>
And then in your AppList component, the Dropdown component needs to use this.props.handleChange instead of this.handleChange:
<DropDownMenu
style={styles.customWidth}
onChange={this.props.handleChange}
>
...
</DropDownMenu>
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
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);
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.
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 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;
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));
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
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>
)
}
}
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>
);
}
<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>
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>
);
};
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!
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} />
}/>
[.....]