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

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} />
}/>
[.....]

Related

How to make a react global state re-render component?

I have tried using two different libraries to handle this issue and both time I get the same behavior.
Libraries tried: OvermindJS, Hookstate
When I click the button I can see the state change is beeing logged in the console but the component will only re-render on the second click
If I change page:
click Home
click Page1
click No Funds
Then it will show 1$
If I click straightway the No Funds button without changing page (first action on page) then the button will not re-render until it is clicked twice.
App.tsx
import * as React from "react"
import {
ChakraProvider,
Box,
Text,
Grid,
theme,
} from "#chakra-ui/react"
import { Switch, Route } from "wouter"
import { Appbar } from "./common/AppBar"
export const App = () => (
<ChakraProvider theme={theme}>
<Appbar />
<Box textAlign="center" fontSize="xl">
<Grid minH="100vh" p={3}>
<Switch>
<Route path="/">
<Text mt={100}>Home</Text>
</Route>
<Route path="/home">
<Text mt={100}>Home</Text>
</Route>
<Route path="/page1">
<Text mt={100}>Page 1</Text>
</Route>
</Switch>
</Grid>
</Box>
</ChakraProvider>
)
AppBar.tsx
import React, { ReactNode } from "react";
import {
Box,
Flex,
HStack,
Link,
IconButton,
Button,
Icon,
useDisclosure,
useColorModeValue,
Stack,
} from '#chakra-ui/react';
import { HamburgerIcon, CloseIcon } from '#chakra-ui/icons';
import { MdAccountBalanceWallet } from 'react-icons/md'
import { useLocation } from 'wouter';
import { actionIncrementFunds, globalState } from "../hookState/state";
import { useState } from "#hookstate/core";
const Links = ['Home', 'Page1'];
const NavLink: React.FC<any> = ({ children, handleClick }: { children: ReactNode, handleClick: any }) => (
<Link
px={2}
py={1}
rounded={'md'}
_hover={{
textDecoration: 'none',
bg: useColorModeValue('red.200', 'red.300'),
}}
onClick={() => handleClick(children)}>
{children}
</Link>
);
export const Appbar: React.FC = () => {
const state = useState(globalState);
const { isOpen, onOpen, onClose } = useDisclosure();
const [location, setLocation] = useLocation();
const handleClick = (path: string) => {
setLocation(`/${path.toLowerCase()}`)
}
const hasFunds = () => {
return state.currentFunds.get() > 0
}
const handleConnectWallet = () => {
actionIncrementFunds()
}
return (
<>
<Box zIndex={900} position={"fixed"} top={0} left={0} width="100%" bg={useColorModeValue('gray.100', 'gray.900')} px={4}>
<Flex h={16} alignItems={'center'} justifyContent={'space-between'}>
<IconButton
size={'md'}
icon={isOpen ? <CloseIcon /> : <HamburgerIcon />}
aria-label={'Open Menu'}
display={{ md: 'none' }}
onClick={isOpen ? onClose : onOpen}
/>
<HStack height={"100%"} spacing={8} alignItems={'center'}>
<HStack
as={'nav'}
spacing={4}
display={{ base: 'none', md: 'flex' }}>
{Links.map((link) => (
<NavLink handleClick={handleClick} key={link}>{link}</NavLink>
))}
</HStack>
</HStack>
<Flex alignItems={'center'}>
{hasFunds()
? <Button
variant={'solid'}
colorScheme={'red'}
size={'md'}
onClick={handleConnectWallet}
mr={4}>
{state.currentFunds.get()} $
</Button>
: <Button
variant={'solid'}
colorScheme={'red'}
size={'md'}
mr={4}
onClick={handleConnectWallet}
leftIcon={<Icon as={MdAccountBalanceWallet} />}>
No Funds
</Button>
}
</Flex>
</Flex>
{
isOpen ? (
<Box pb={4} display={{ md: 'none' }}>
<Stack as={'nav'} spacing={4}>
{Links.map((link) => (
<NavLink handleClick={handleClick} key={link}>{link}</NavLink>
))}
</Stack>
</Box>
) : null
}
</Box >
</>
);
}
Behavior example:
Repo with example for Hookstate:
https://github.com/crimson-med/state-issue
Repo with example for OvermindJS:
https://github.com/crimson-med/state-issue/tree/overmind
How can I get the button on change on the first click?

Idiomatic way to handle Message Alerts state in React and Material-UI

I basically am building a web application using react and material UI. I have a registration form.
When this registration form successfully submits, it should display a success message at the top of the page.
Now, I have cordoned off the top of every page to show the MyAlert component. I have done this to make my site more consistent. Basically whenever there is a message to show to the user, it will render there.
Now, the issue is once the message is shown, if I navigate around my site, there is no way to turn it off.
Could someone please help explain what the best way to show a message once, then hide it would be? Basically once the user registers successfully, show a success message. Once the user navigates to a new page, hide the message at the top until a new message needs to be shown.
I imagine this is a very common design pattern, so I must be missing something in terms of React knowledge. Even best practice design patterns, or links to how it can be handled would be appreciated.
My toy app looks as follows:
App.jsx
import * as React from 'react'
import { useState, useEffect } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { Alert } from '#material-ui/lab'
import MenuIcon from '#material-ui/icons/Menu'
import {
Button,
AppBar,
Toolbar,
IconButton,
Typography,
makeStyles,
Container,
} from '#material-ui/core'
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
}))
const Navbar = (props) => {
const classes = useStyles()
return (
<div>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
<Button color="inherit" component={Link} to={'/'}>
My App
</Button>
</Typography>
<Button color="inherit" component={Link} to={'/register'}>
Register
</Button>
</Toolbar>
</AppBar>
</div>
)
}
const Register = (props) => {
const classes = useStyles()
const { setShowAlert, setAlertMessage, setAlertSeverity } = props
useEffect(() => {
console.log('use effected')
}, [])
const handleRegistration = () => {
setAlertMessage(
'You have succesfully registered! Please check your email for a verification link.'
)
setAlertSeverity('success')
setShowAlert(true)
}
return (
<Container maxWidth="sm" align="center" className={classes.root}>
This is some kind of form. I have redacted all input fields to
illustrate the issue more clearly.<br></br>
<Button
variant="contained"
color="primary"
onClick={handleRegistration}
>
Register
</Button>
</Container>
)
}
const MyAlert = (props) => {
const { severity, message } = props
return <Alert severity={severity}>{message}</Alert>
}
const App = () => {
const [alertSeverity, setAlertSeverity] = useState('')
const [alertMessage, setAlertMessage] = useState('')
const [showAlert, setShowAlert] = useState(false)
return (
<BrowserRouter>
<div className="app">
<Navbar />
<div>{showAlert ? 'true' : 'false'}</div>
{showAlert ? (
<MyAlert severity={alertSeverity} message={alertMessage} />
) : (
''
)}
<Switch>
<Route
path="/register"
render={(props) => (
<Register
{...props}
setShowAlert={setShowAlert}
setAlertMessage={setAlertMessage}
setAlertSeverity={setAlertSeverity}
/>
)}
/>
</Switch>
</div>
</BrowserRouter>
)
}
export default App
index.jsx
import * as React from "react"
import { render } from "react-dom"
import App from './App';
render(<App />, document.getElementById("app"))
Wouldn't that just be the case of using a setTimeout? When MyAlert is rendered, show it for 5 seconds and then dismiss.
First, pass it as a prop to your component (I also proposed a slightly different conditional render):
<BrowserRouter>
<div className="app">
<Navbar />
<div>{showAlert ? 'true' : 'false'}</div>
{showAlert &&
<MyAlert severity={alertSeverity} message={alertMessage} setShowAlert = {setShowAlert}/> }
<Switch>
<Route
path="/register"
render={(props) => (
<Register
{...props}
setShowAlert={setShowAlert}
setAlertMessage={setAlertMessage}
setAlertSeverity={setAlertSeverity}
/>
)}
/>
</Switch>
</div>
</BrowserRouter>
Then in your MyAlert component, use useEffect to trigger a timeout:
const MyAlert = (props) => {
useEffect(() => {
const timeout = setTimeout(() => {
props.setShowAlert(false); // Disable your alert after 5 seconds
}, 5000);
return () => {
clearTimeout(timeout); // Clears timer in case you close your alert somewhere else.
}
}, [])
}

Logout button in custom usermenu does nothing

I have custom user menu
import React from "react";
import { UserMenu, MenuItemLink} from "react-admin";
import SettingsIcon from "#material-ui/icons/Settings";
import jwt from "jwt-decode";
const AdminUserMenu = ({ logout }) => {
var user = jwt(localStorage.getItem("token"));
return (
<UserMenu>
<MenuItemLink
to={"/Admins/" + user.id}
primaryText="Profile"
leftIcon={<SettingsIcon />}
/>
{logout}
</UserMenu>
);
};
export default AdminUserMenu;
const AdminAppBar = (props) => (
<AppBar {...props} userMenu={<AdminUserMenu />} />
);
Profile link works. But logout button does nothing. How can I make it work so that user could logout?
Try this:
const AdminUserMenu = (props) => {
var user = jwt(localStorage.getItem("token"));
return (
<UserMenu {...props} >
<MenuItemLink
to={"/Admins/" + user.id}
primaryText="Profile"
leftIcon={<SettingsIcon />}
/>
</UserMenu>
);
};
I had the same behavior that you did using the latest version. The above code from MaxAlex also didn't work for some reason.
I was able to get it work work as follows. Notice the placement of the {logout} in the ConfigurationMenu object versus in the CustomUserMenu.
import * as React from 'react';
import { forwardRef } from 'react';
import { AppBar, UserMenu, MenuItemLink, useTranslate } from 'react-admin';
import Typography from '#material-ui/core/Typography';
import SettingsIcon from '#material-ui/icons/Settings';
import { makeStyles } from '#material-ui/core/styles';
import Logo from '../Logo';
const useStyles = makeStyles({
title: {
flex: 1,
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
},
spacer: {
flex: 1,
}
});
const ConfigurationMenu = ({logout, props}) => {
const translate = useTranslate();
return (
<div>
<MenuItemLink
to="/profile"
primaryText={translate("cw.profile.title")}
leftIcon={<SettingsIcon />}
{...props}
/>
{logout}
</div>
);
};
const CustomUserMenu = (props) => (
<UserMenu {...props} >
<ConfigurationMenu />
</UserMenu>
);
const CustomAppBar = (props) => {
const classes = useStyles();
return (
<AppBar {...props} elevation={1} userMenu={<CustomUserMenu />}>
<Typography
variant="h6"
color="inherit"
className={classes.title}
id="react-admin-title"
/>
<Logo />
<span className={classes.spacer} />
</AppBar>
);
};
export default CustomAppBar;

Component does not change state when going from dynamic route to static

I have a route /users that prints a list of users, each user is a link to a dynamic route /users/:cn that displays the user details.
Navigating from /users to /users/:cn is possible but from /users/:cn to /users is not, the url changes but nothing happens, and in the backend log there is no get request to the/users page . But if i am on /users/:cn and navigate to /otherRoute it works fine, or if i am on /users and navigate to /otherRoute also work fine. I think i need to update the state somehow when i am on the dynamic route. Any help?
I already tried wrapping the component userWrapper with withRouter but that does not work.
users.js
import React ,{Component} from "react"
import { BrowserRouter as Router,Switch, Route, withRouter} from "react-router-dom"
import Layout from "../components/layout"
import User from "../components/users"
import MainComponentWrapper from "../components/mainComponentWrapper"
import UserDetails from "../components/userDetails"
const userDetailsWrapper = props => {
const url = "http://localhost:5000/user/" + props.match.params.cn
return (
<MainComponentWrapper url={url}>
<UserDetails />
</MainComponentWrapper>
)
}
const userWrapper = props => {
const url = "http://localhost:5000/user"
return (
<MainComponentWrapper url={url}>
<User />
</MainComponentWrapper>
)
}
const IndexPage = () => (
<div>
<Router >
<Layout>
<Switch>
<Route exact path="/users" component={(userWrapper)}/>
<Route exact path="/users/:cn" component={userDetailsWrapper} />
</Switch>
</Layout>
</Router>
</div>
)
export default IndexPage
layout.jsx
import PropTypes from "prop-types"
import React from "react"
import { makeStyles } from "#material-ui/core/styles"
import Drawer from "#material-ui/core/Drawer"
import CssBaseline from "#material-ui/core/CssBaseline"
import List from "#material-ui/core/List"
import Divider from "#material-ui/core/Divider"
import ListItem from "#material-ui/core/ListItem"
import ListItemIcon from "#material-ui/core/ListItemIcon"
import ListItemText from "#material-ui/core/ListItemText"
import ExitToApp from "#material-ui/icons/ExitToApp"
import PeopleIcon from "#material-ui/icons/People"
import ListIcon from "#material-ui/icons/List"
import ComputerIcon from "#material-ui/icons/Computer"
import { navigate } from "gatsby"
const drawerWidth = 240
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
},
drawer: {
width: drawerWidth,
flexShrink: 0,
},
drawerPaper: {
width: drawerWidth,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
toolbar: theme.mixins.toolbar,
}))
function Layout({ children }) {
const classes = useStyles()
const handleLogout = event => {
localStorage.removeItem("access_token")
window.location.replace("/")
}
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
className={classes.drawer}
variant="permanent"
classes={{
paper: classes.drawerPaper,
}}
>
<div className={classes.toolbar} />
<List>
<ListItem button onClick={() => navigate("/users")}>
<ListItemIcon>
<PeopleIcon />
</ListItemIcon>
<ListItemText primary={"Users"} />
</ListItem>
<ListItem button onClick={() => navigate("/machines")}>
<ListItemIcon>
<ComputerIcon />
</ListItemIcon>
<ListItemText primary={"Machines"} />
</ListItem>
<ListItem button onClick={() => navigate("/audits")}>
<ListItemIcon>
<ListIcon />
</ListItemIcon>
<ListItemText primary={"Audit"} />
</ListItem>
</List>
<Divider />
<List>
<ListItem button onClick={handleLogout}>
<ListItemIcon>
<ExitToApp />
</ListItemIcon>
<ListItemText primary={"Logout"} />
</ListItem>
</List>
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
{children}
</main>
</div>
)
}
Layout.propTypes = {
children: PropTypes.node.isRequired,
}
export default Layout
MainComponentWrapper.jsx
import React from "react"
import { makeStyles } from "#material-ui/core/styles"
import CircularProgress from "#material-ui/core/CircularProgress"
const useStyles = makeStyles(theme => ({
root: {
width: "100%",
},
progress: {
margin: theme.spacing(2),
},
}))
export default function MainComponentWrapper({ url, children }) {
const classes = useStyles()
const [data, setData] = React.useState()
React.useEffect(() => {
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + window.localStorage.getItem("access_token"),
},
})
.then(resp => {
return resp.json()
})
.then(fetchedData => {
setData(fetchedData)
})
.catch(error => {
console.log(error)
window.localStorage.removeItem("access_token")
window.location.replace("/")
})
}, [url])
if (data === undefined) {
return (
<div className={classes.root}>
<CircularProgress className={classes.progress} />
</div>
)
}
return (
<div className={classes.root}>
{React.cloneElement(children, { data: data })}
</div>
)
}
Using #reach/router instead of react-router-dom solved the issue for the author. Gatsby library that was used to navigate the pages is built over #reach/router thus using react-router-dom caused conflicts. Code changes for #react/router
import { Router } from "#reach/router"
...
<Router>
<UserWrapper path="/users" />
<UserDetailsWrapper path="/users/:cn" />
</Router>
Read comments for details. Answer by author can be found below.
Discarded
All seems well could you please also share the code for your <MainComponentWrapper url={url}>. How are you calling the API there specifically in which lifecycle method?
So i fixed the problem, #m.sohail was right.
I had conflicts using react-router-dom, I need to use #reach/router, and made some code changes.
import React from "react"
import Layout from "../components/layout"
import User from "../components/users"
import MainComponentWrapper from "../components/mainComponentWrapper"
import UserDetails from "../components/userDetails"
import { Router } from "#reach/router"
const IndexPage = () => (
<div>
<Layout>
<Router>
<UserWrapper path="/users" />
<UserDetailsWrapper path="/users/:cn" />
</Router>
</Layout>
</div>
)

Render components on Material UI tabs

I have been trying to render certain components on clicking inside a tab content.
This is what i have been doing:
import React, { Component } from "react";
export class Global extends Component {
render() {
return <div>Global</div>;
}
}
export default Global;
This is a basic component to show.
import React, { Component } from "react";
import PropTypes from "prop-types";
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 Toolbar from "#material-ui/core/Toolbar";
import Button from "#material-ui/core/Button";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import IconButton from "#material-ui/core/IconButton";
import SearchIcon from "#material-ui/icons/Search";
import Link from "#material-ui/core/Link";
import Global from "../Dashboard/Global";
import Graficos from "../Dashboard/Graficos";
import Individual from "../Dashboard/Individual";
import Temporais from "../Dashboard/Temporais";
import CSSTransitionGroup from "react-addons-css-transition-group";
import { Paper } from "#material-ui/core";
import { classes } from "../constants/tabs";
function TabContainer(props) {
return (
<Typography
component="div"
style={{
padding: 8 * 3
}}
>
{" "}
{props.children}{" "}
</Typography>
);
}
TabContainer.propTypes = {
children: PropTypes.node.isRequired
};
//const sections = ["Indicador 1", "Indicador 2", "Indicador 2", "Indicador 2"];
const PageShell = (Page, previous) => {
return props => (
<div className="page">
<CSSTransitionGroup
transitionAppear={true}
transitionAppearTimeout={600}
transitionEnterTimeout={600}
transitionLeaveTimeout={600}
transitionName={props.match.path === "/one" ? "SlideIn" : "SlideOut"}
>
{console.log(props)}
<Page {...props} />
</CSSTransitionGroup>
</div>
);
};
function ItemOne(theme) {
return (
<Paper>
<div>Item 1</div>
</Paper>
);
}
function ItemTwo(theme) {
return (
<Paper>
<div>Item two</div>
</Paper>
);
}
export default function Header() {
const [value, setValue] = React.useState(0);
function handleChange(event, newValue) {
setValue(newValue);
}
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs value={value} onChange={handleChange}>
{/* <Tabs value={value} onChange={handleChange}>
{sections.map(section => (
<Tab label={section} />
))}
</Tabs> */}
<Tab label="Item One" component={Link} to="/global" />
<Tab label="Item Two" component={Link} to="/individual" />
<Tab label="Item Three" component={Link} to="/temporal" />
<Tab label="Item Four" component={Link} to="/graficos" />
</Tabs>
</AppBar>
<Switch>
<Route path="/global" component={PageShell(ItemOne)} />
<Route path="/individual" component={PageShell(ItemTwo)} />
</Switch>
{/* {value === 0 && <TabContainer>Item One</TabContainer>}
{value === 1 && <TabContainer>Item Two</TabContainer>}
{value === 2 && <TabContainer>Item Three</TabContainer>}
{value === 3 && <TabContainer>Item Four</TabContainer>} */}
</div>
);
}
This is my material header and im trying to load that global component below my header. The idea is that my header is fixed and the component renders on tab click. Can anyone help me with what im doing wrong ?

Resources