如何正确处理基于路由的条件组件渲染?

7

我在网上和SO上搜索了很多,还在reactiflux聊天中询问过,但没有找到一种干净且不像hack的方式来根据路由/路径渲染某些组件。

假设我有一个<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

是的,我可以将路由存储在数组中并编写循环,这将使代码更具可重用性。但这是否是处理此用例的最佳和最优雅的方式呢?

我认为这甚至可能存在错误,例如如果我不为具有路由/xyz的页面呈现标题,并且我具有带有UUID的路由,例如/projects/:idid=xyzfoo,那么/projects/xyzfoo将不会显示标题,但它应该。

6个回答

7
为了实现DRY原则(避免代码重复)并根据路由实现条件渲染,您应该按照以下结构进行操作:
步骤1) 创建布局(HOC),它将返回带有 <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} />
        </>
    )} />
)

第二步)导入布局并使用

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>

        )
    }
}

输出:

path-test1

path-test2

结论:

因此,每当您想要在路由定义组件中包含标题组件时,请使用<HeaderLayout />,如果您不想使用标题,则只需使用<Route /> 来在页面中隐藏标题。


6
你可以先列出没有标题的所有路由,并在额外的开关中分组其他路由:
...
<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>
)

根据文档

没有路径的路线总是匹配。

不幸的是,这种解决方案可能会在“未找到”页面中出现问题。它应该放置在 HeaderRoutes 的末尾,并将与一个 Header 一起呈现。 Dhara's solution 没有这种问题。但如果React Router内部发生变化,则可能无法与 Switch 很好地配合使用:

<Switch> 的所有子元素都应该是 <Route><Redirect> 元素。只有第一个匹配当前位置的子元素将被渲染。

通过 HOC 将 `Route` 覆盖不是一个 Route 本身。但它应该能正常工作,因为当前代码库实际上期望任何 React.Element 具有与 <Route><Redirect> 相同的 props 语义。

哇,我得检查一下,如果它能像那样工作,那正是我所需要的! :) - flppv
我已经检查过了,它可以正常工作,谢谢!这是最优雅的方式,没有任何代码重复。我很惊讶它是如何工作的,我会感激一下关于我们如何在没有路径“prop”的情况下使用Route的解释。 - flppv
谢谢您的解释!我也喜欢Dhara的解决方案(还有jdc91的,他最先使用了HOC),但我发现您的代码重复最少。 - flppv
你也可以使用属性 path="*",尽管它似乎有点多余。 - JonShipman

4

我认为你的看法有误。在React中,组件可组合性是最重要的特点。头部是一个可重用的组件,可以放置在任何你想要的地方!

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

比如说,你设计了应用程序的几个页面路由。头部是这些页面中使用的任何子组件!

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

现在,在每个页面中,你可以在需要的地方引入 Header 组件来添加页眉。
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>
    );
}

一个更加优雅但略微复杂的方法是引入高阶组件。这将使您在路由级别上添加标题的意图更加清晰!
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 并在各处使用它看起来像是代码重复的一大部分,这无法解决让代码更少重复的问题。HOC 看起来好一些,但仍然出现了一些可重复的部分。是否有可能使用 某些东西 包装多个 <Route> 元素,以便该包装器内的每个路由都可以呈现带有 Header 的组件? - flppv

2
将这些关于头部的数据作为路由参数或查询参数进行包含。

/projects/:headerBool/:id

或者:

/projects/:id?header=false

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

将"Original Answer"翻译成"最初的回答"。

我不能说这是最好的解决方案,因为我不想在路由中存储Header“state”,而且任何人都可以用他需要的东西替换它。 - flppv
当你将路径名按 '/' 分割并查找每个部分的精确匹配时,可以减少出现错误的可能性。如果您以这种方式进行操作,最好创建一个对象,将字符串作为键,将值设置为 'true',这样您只需在迭代过程中查找每个部分即可。 - Craig Gehring

0
你可以使用 Route 的 render 属性。 例如:
<Route path='/pageWithoutHeader' render={() => <Page1 />} />
<Route path='pageWithHeader' render={() => 
      <Header/>
      <Page2 />}
/>

使用此方法要比将头部组件放在页面内更好。


0
我经常碰到的一个情景是,数据库中的某些页面具有Header = false或Page Title = false。对此的一个好解决方案是使用Context。
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,让你的代码更加优雅。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接