根据路由处理条件组件渲染的正确方法?

Proper way of handling conditional component render depending on route?

我在 Web 和 SO 上搜索了很多,在 reactiflux chat 中询问,但没有找到干净且 non-hack-looking 渲染某些组件的方式 route/path.

假设我有 <Header />,它应该显示在某些页面上,而应该隐藏在其他页面上。

当然我可以在 Header 组件中使用这个字符串

if (props.location.pathname.indexOf('/pageWithoutAHeader') > -1) return null

完全没问题,如果 /pageWithoutAHeader 是唯一的。如果我需要 5 个页面的相同功能,它会变成这样:

if (props.location.pathname.indexOf('/pageWithoutAHeader1') > -1) return null
if (props.location.pathname.indexOf('/pageWithoutAHeader2') > -1) return null
if (props.location.pathname.indexOf('/pageWithoutAHeader3') > -1) return null

是的,我可以把路由存入数组,写个循环,这样会更code-reusable。但这是处理此用例的最佳和最优雅的方式吗?

我相信它甚至可能有问题,例如,如果我不为具有路由 /xyz 的页面呈现 header 并且我有带有 UUID 的路由,例如 /projects/:id,和 id=xyzfoo,因此 /projects/xyzfoo 不会显示 header,但它应该显示。

将有关 header 的数据作为路由参数或查询包含在内。

/projects/:header布尔/:id

或者:

/projects/:id?header=false

然后您可以通过props.match或props.location访问它。

我认为你看错了。因此,在考虑 React 时,可组合性是首要特征。 header 是一个可重复使用的组件,可以放在任何你想要的地方!

这样思考会给你提供多种选择。

假设您有几个为应用程序设计的页面路由。页眉是任何使用它的页面的子组件!

function AppRouter() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about/">About</Link>
            </li>
            <li>
              <Link to="/users/">Users</Link>
            </li>
          </ul>
        </nav>

        <Route path="/" exact component={Index} />
        <Route path="/about/" component={About} />
        <Route path="/users/" component={Users} />
      </div>
    </Router>
  );
}

现在,在每个需要页眉的页面中,您可以在必要时引入页眉组件。

export default function Index(){
    return (
        <React.Fragment>
             <Header/>
             <div> ... Index Content </div>
        </React.Fragment>
    );
}

export default function About(){
    return (
        <React.Fragment>
             //I don't need a header here.
             <div> ... Index Content </div>
        </React.Fragment>
    );
}

An even more elegant but a bit more complex approach would be to introduce a Higher order component. This would make your intentions more clear on adding headers at the route level!

function withHeader(Page){
    return class extends React.Component {
        render() {
          // Wraps the input component in a container, without mutating it.
          return (
              <React.Fragment>
                 <Header/>
                 <Page {...this.props} />);
              </React.Fragment>
        }
    }
}

function AppRouter() {
  return (
    <Router>
      <div>
        <nav>
          <ul>
            <li>
              <Link to="/">Home</Link>
            </li>
            <li>
              <Link to="/about/">About</Link>
            </li>
            <li>
              <Link to="/users/">Users</Link>
            </li>
          </ul>
        </nav>

        <Route path="/" exact component={withHeader(Index)} />
        <Route path="/about/" component={About} />
        <Route path="/users/" component={Users} />
      </div>
    </Router>
   );
}

您可以先列出所有没有 header 的路由,然后将其他路由分组到其他开关中:

...
<Switch>
  <Route path="/noheader1" ... />
  <Route path="/noheader2" ... />
  <Route path="/noheader3" ... />
  <Route component={HeaderRoutes} />
</Switch>
...

HeaderRoutes = props => (
  <React.Fragment>
    <Header/>
    <Switch>
      <Route path="/withheader1" ... />
      <Route path="/withheader2" ... />
      <Route path="/withheader3" ... />
    </Switch>
  </React.Fragment>
)

来自documentation

Routes without a path always match.

不幸的是,此解决方案可能对 "not found" 页面有问题。它应该放在 HeaderRoutes 的末尾,并将用 Header.

呈现

Dhara's solution没有这样的问题。但如果 R​​eact Router 内部发生变化,它可能无法与 Switch 一起正常工作:

All children of a <Switch> should be <Route> or <Redirect> elements. Only the first child to match the current location will be rendered.

Route 上的 HOC 本身不是 Route。但它应该可以正常工作,因为当前的代码库实际上期望任何 React.Element 具有与 <Route><Redirect> 具有相同的道具语义。

为了实现DRY规则(避免代码重复),根据路由实现条件渲染,你应该在以下结构上工作:

步骤 1) 创建布局 (HOC),其中 returns 使用 <Header/> 给定组件并将其导出

import React from "react"
import { Route } from "react-router-dom"
import Header from "./Header"

export const HeaderLayout = ({component: Component, ...rest}) => (
    <Route {...rest} render={(props) => (
        <>
            <Header/>
            <Component {...props} />
        </>
    )} />
)

步骤 2) 导入布局并使用它

import React, { Component } from 'react'
import { BrowserRouter, Route, Switch } from "react-router-dom"
import Test1 from './Test1';
import Test2 from './Test2';
import { HeaderLayout } from './HeaderLayout';

export default class Main extends Component {
    render() {
        return (
            <BrowserRouter>
                <Switch>
                    <HeaderLayout path="/test1" component={Test1} />
                    <Route path="/test2" component={Test2}/>
                </Switch>
            </BrowserRouter>

        )
    }
}

输出:

结论:

因此,无论何时您想要将页眉组件与路由定义的组件一起使用,请使用 <HeaderLayout />,如果您不想使用页眉,则只需使用 <Route /> 即可在您的页面中隐藏页眉.

您可以使用路由的渲染属性。 示例:

<Route path='/pageWithoutHeader' render={() => <Page1 />} />
<Route path='pageWithHeader' render={() => 
      <Header/>
      <Page2 />}
/>

这种方法比在页面内部使用页眉组件要好。

我经常想到的一个场景是数据库中的某些页面具有 Header = false 或 Page Title = false。一个好的解决方案是上下文。

import React, { createContext, useContext, useState, useEffect } from "react";
import { Switch, Route } from "react-router-dom";

const AppContext = createContext({});

const Header = () => {
  const { headerActive } = useContext(AppContext);

  if (!headerActive) {
    return null;
  }

  return (
    <header>I'm a header</header>
  );
}

const PageWithHeader = () => {
  const { setHeaderActive } = useContext(AppContext);
  useEffect(() => {
    setHeaderActive(true);
  },[setHeaderActive]);

  return (
    <div>Page with header</div>
  );
}

const PageWithoutHeader = () => {
  const { setHeaderActive } = useContext(AppContext);
  useEffect(() => {
    setHeaderActive(false);
  },[setHeaderActive]);

  return (
    <div>Page without header</div>
  );
}

export const App = () => {
  const [headerActive, setHeaderActive] = useState(true);

  return (
    <AppContext.Provider value={{ headerActive, setHeaderActive }}>
      <Header />
      <Switch>
        <Route path="page-1">
          <PageWithHeader />
        </Route>
        <Route path="page-2">
          <PageWithoutHeader />
        </Route>
      <Switch>
    </AppContext.Provider>
  );
}

您还可以使用自定义挂钩进一步简化它。类似于:

export const useHeader = (active) => {
  const { headerActive, setHeaderActive } = useContext(AppContext);
  useEffect(() => {
    if (active !== undefined) {
      setHeaderActive(active);
    }
  },[setHeaderActive, active]);

  return headerActive;
}

那么在原来的组件中:

const Header = () => {
  const headerActive = useHeader();

  if (!headerActive) {
    return null;
  }

  return (
    <header>I'm a header</header>
  );
}
...
const PageWithoutHeader = () => {
  useHeader(false);

  return (
    <div>Page without header</div>
  );
}

您也可以花点心思,将 useLocation 钩子与 useRef 结合使用来跟踪以前和当前的路径名,这样您就可以拥有默认状态,而无需在每个页面中声明 useHeader。