关于react js:与React Router 4的Material-UI Tabs集成?

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

新的react-router语法使用Link组件在路由之间移动。但是如何将其与material-ui集成?

就我而言,我将标签用作主要的导航系统,因此从理论上讲,我应该具有以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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>
    )
  }
}

但是在渲染时,material-ui会引发错误,指出Tabs的子级必须是Tab组件。可能的方式是什么?如何管理选项卡的isActive道具?

预先感谢


另一种解决方案(https://codesandbox.io/s/l4yo482pll),既没有处理程序也没有HOC,仅是纯react-router和material-ui组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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>
     
        <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={() => Tab 2} />
                <Route path={allTabs[2]} render={() => Tab 3} />
                <Route path={allTabs[0]} render={() => Tab 1} />
              </Switch>
            </Fragment>
          )}
        />
     
    </BrowserRouter>
  );
}

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


我的老师帮助我使用了React Router 4.0的withRouter来包装Tabs组件以启用类似这样的历史记录方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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="/"
        >
       
           <Home />
       
        </Tab>
        <Tab
          label="Portfolio"
          value="/portfolio"
            >
         
            <Portfolio />
         
        </Tab>
      </Tabs>          
    )
  }
}

export default withRouter(NavTabs)

只需将BrowserRouter添加到index.js,您就可以进行了。


您从material-ui中看到的错误是因为它希望将<Tab>组件呈现为<Tabs>组件的直接子代。

现在,这是我发现的一种将链接集成到<Tabs>组件中而不丢失样式的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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>
        );
    }
}

要管理选项卡的'active'属性,可以在<Tabs>组件中使用value属性,并且每个选项卡还需要具有value属性,因此当两个属性都匹配时,则会将活动样式应用于该标签。


这是另一种解决方案,使用Material 1.0的beta版并将浏览器"前进/后退"添加到组合中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 {props.children};
}

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 (
     
        <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>}
     
    );
  }
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 <BrowserRouter>

  <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>


正如@gkatchmar所说,您可以使用withRouter高阶组件,但也可以使用context API。因为@gkatchmar已经显示了Router,所以我只会显示context API。请记住,这是一个实验性API。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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="/"
        >
       
           <Home />
       
        </Tab>
        <Tab
          label="Portfolio"
          value="/portfolio"
            >
         
            <Portfolio />
         
        </Tab>
      </Tabs>          
    )
  }
}

您可以使用browserHistory代替React-Router Link组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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>

如您所见,您只需将目标push() browserHistory


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import React, { useContext, useEffect } from"react";
import PropTypes from"prop-types";
import Drawer from"@material-ui/core/Drawer";
import IconButton from"@material-ui/core/IconButton";
import MenuIcon from"@material-ui/icons/Menu";
import Typography from"@material-ui/core/Typography";
import useStyles from"./Styles";
import Tabs from"@material-ui/core/Tabs";
import Tab from"@material-ui/core/Tab";
import Box from"@material-ui/core/Box";
import { __t } from"core/translation/translation";
import BrowserData from"core/helper/BrowserData";
import profileMenuItems from"./MenuItems";
import LayoutContext from"components/layout/core/LayoutContext";
import { useHistory, useParams } from"react-router-dom";

function TabPanel(props) {
  const { children, value, index, ...other } = props;
  return (
    <div
      role="tabpanel"
      hidden={value !== index}
      id={`vertical-tabpanel-${index}`}
      aria-labelledby={`vertical-tab-${index}`}
      {...other}
    >
      {value === index && (
        <Box p={3}>
          <Typography>{children}</Typography>
        </Box>
      )}
   
  );
}

TabPanel.propTypes = {
  children: PropTypes.node,
  index: PropTypes.any.isRequired,
  value: PropTypes.any.isRequired,
};

export default function UserProfile(props) {
  const { window } = props;
  const classes = useStyles();
  const history = useHistory();
  const { page } = useParams();
  const { isDesktop } = useContext(LayoutContext);
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState(0);

  const handleChange = (event, newValue) => {
    setValue(newValue);
    history.push("/yourPath/" + newValue);
  };

  useEffect(() => {
    if (!!page) {
      setValue(eval(page));
    }
  }, [page]);


  const getContent = () => {
    const { component: Component } = MenuItems[value];
    return <Component />;
  };

  const handleDrawerToggle = () => {
    setOpen((prevState) => !prevState);
  };

  const Menu = (
   
      <Tabs
        orientation="vertical"
        variant="scrollable"
        value={value}
        onChange={handleChange}
        className={classes.tabs}
      >
        {MenuItems.map(
          ({ label, iconPath, iconClassName ="" }, index) => (
            <Tab
              label={label}
              id={`vertical-tab-${index}`}
              aria-controls={`vertical-tabpanel-${index}`}
              className={classes.tab}
              icon={
                <img className={iconClassName} src={iconPath} alt={label} />
              }
            />
          )
        )}
      </Tabs>
   
  );

  return (
   
      <IconButton
        color="inherit"
        aria-label="open drawer"
        edge="start"
        onClick={handleDrawerToggle}
        className={classes.drawerToggleButton}
      >
        <MenuIcon />
      </IconButton>

      <nav className={classes.drawer}>
        <Drawer
          anchor="left"
          open={isDesktop ? true : open}
          onClose={handleDrawerToggle}
          variant={isDesktop ?"permanent" :"temporary"}
          classes={{
            paper: classes.drawerPaper,
          }}
          ModalProps={{
            keepMounted: true,
          }}
        >
          {Menu}
        </Drawer>
      </nav>

      <main className={classes.content}>
        <TabPanel
          value={value}
          key={value}
          index={value}
          className={classes.tabPanel}
        >
          {getContent()}
        </TabPanel>
      </main>
   
  );
}

我在我的应用中以这种方式运行它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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>
            )}
       
    );
}

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 (
       
            <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>
       
    );
}

...并在React Router中:

1
2
3
4
5
6
7
8
9
10
11
12
[.....]
<Route exact path="/tab_1"
       render={(routeProps) =>
           <MUITabPlusReactRouterDemo history={routeProps.history}
           />
       }/>

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