保持 Material UI 选项卡和 React Router 同步

Keeping Material UI tabs and React Router in sync

有没有一种简单的方法可以使 Material UI 选项卡和 React 路由器保持同步?

基本上,我想在用户单击选项卡 [1] 时更改 URL 并且当用户导航到具有非选项卡的不同页面时选项卡应自动更改 link 或按钮,当然还有直接访问 [2] 和页面刷新。

此外,如果 React 路由器的 非精确 功能也很好,那么 /foo 选项卡应该对 /foo/foo 都有效/foo/bar/1.

[1] 其他 SO 答案建议直接使用历史 api,这是 react-router 的好习惯吗?

[2] 我不确定它叫什么,我的意思是当用户直接加载 /foo 而不是加载 / 然后导航到 /foo 时选项卡或 link


编辑:

我创建了一个包装器组件来完成这项工作,但有一些问题:

class CustomTabs extends React.PureComponent {
    constructor() {
        super();

        this.state = {
            activeTab: 0
        }
    }

    setActiveTab(id) {
        this.setState({
            activeTab: id
        });
        return null;
    }

    render() {
        return (
            <div>
                {this.props.children.map((tab,index) => {
                    return (
                        <Route
                            key={index}
                            path={tab.props.path||"/"}
                            exact={tab.props.exact||false}
                            render={() => this.setActiveTab(index)}
                        />
                    );
                })}
                <Tabs
                    style={{height: '64px'}}
                    contentContainerStyle={{height: '100%'}}
                    tabItemContainerStyle={{height: '100%'}}
                    value={this.state.activeTab}
                >
                    {this.props.children.map((tab,index) => {
                        return (
                            <Tab
                                key={index}
                                value={index}
                                label={tab.props.label||""}
                                style={{paddingLeft: '10px', paddingRight: '10px', height: '64px'}}
                                onActive={() => {
                                    this.props.history.push(tab.props.path||"/")
                                }}
                            />
                        );
                    })}
                </Tabs>
            </div>
        );
    }
}

我是这样使用它的:

<AppBar title="Title" showMenuIconButton={false}>
    <CustomTabs history={this.props.history}>
        <Tab label="Home" path="/" exact/>
        <Tab label="Foo" path="/foo"/>
        <Tab label="Bar" path="/bar"/>
    </CustomTabs>
</AppBar>

但是:

Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.

我认为这是因为我在调用 render() 后立即设置状态 - 因为 Route.render,但我不知道如何解决这个问题。


编辑#2

我终于解决了所有问题,但方式有点老套。

class CustomTabsImpl extends PureComponent {
    constructor() {
        super();

        this.state = {
            activeTab: 0
        }
    }

    componentWillMount() {
        this.state.activeTab = this.pathToTab(); // eslint-disable-line react/no-direct-mutation-state
    }

    componentWillUpdate() {
        setTimeout(() => {
            let newTab = this.pathToTab();
            this.setState({
                activeTab: newTab
            });
        }, 1);
    }

    pathToTab() {
        let newTab = 0;

        this.props.children.forEach((tab,index) => {
            let match = matchPath(this.props.location.pathname, {
                path: tab.props.path || "/",
                exact: tab.props.exact || false
            });
            if(match) {
                newTab = index;
            }
        });

        return newTab;
    }

    changeHandler(id, event, tab) {
        this.props.history.push(tab.props['data-path'] || "/");
        this.setState({
            activeTab: id
        });
    }

    render() {
        return (
            <div>
                <Tabs
                    style={{height: '64px'}}
                    contentContainerStyle={{height: '100%'}}
                    tabItemContainerStyle={{height: '100%'}}
                    onChange={(id,event,tab) => this.changeHandler(id,event,tab)}
                    value={this.state.activeTab}
                >
                    {this.props.children.map((tab,index) => {
                        return (
                            <Tab
                                key={index}
                                value={index}
                                label={tab.props.label||""}
                                data-path={tab.props.path||"/"}
                                style={{height: '64px', width: '100px'}}
                            />
                        );
                    })}
                </Tabs>
            </div>
        );
    }
}

const CustomTabs = withRouter(CustomTabsImpl);

你可以使用 react routers NavLink 组件

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

<NavLink
  activeClassName="active"
  to="/foo"
>Tab 1</NavLink>

当 /foo 是路由时,active class 将添加到此 link。 NavLink 也有一个 isActive 道具,可以传递一个函数来进一步自定义确定 link 是否处于活动状态的功能。

https://reacttraining.com/react-router/web/api/NavLink

首先,感谢您回答您的问题。 我以不同的方式处理了这个问题,我决定 post 在这里感谢社区的赞赏。

我的推理是:"It would be simpler if I could tell the Tab instead the Tabs component about which one is active."

实现这一点非常简单,可以通过为 Tabs 组件设置一个已知的固定值并将该值分配给任何应该处于活动状态的选项卡来实现。

此解决方案要求托管选项卡的组件可以访问 react-router 中的位置和匹配等道具,如下所示


首先,我们创建一个工厂函数,从渲染方法中删除臃肿的代码。如果所需的路由匹配,这里将固定的 Tabs 值设置为 Tab,否则我只是抛出一个任意常量,例如 Infinity。

const mountTabValueFactory = (location, tabId) => (route) => !!matchPath(location.pathname, { path: route, exact: true }) ? tabId : Infinity;


之后,您只需将信息插入渲染函数即可。

render() {
   const {location, match} = this.props;
   const tabId = 'myTabId';
   const getTabValue = mountTabValueFactory(location, tabId);

   return (
     <Tabs value={tabId}>
       <Tab
         value={getTabValue('/route/:id')}
         label="tab1"
         onClick={() => history.push(`${match.url}`)}/>
       <Tab
         value={getTabValue('/route/:id/sub-route')}
         label="tab2"
         onClick={() => history.push(`${match.url}/sub-route`)}
       />
     </Tabs>
   )
}