反应防止重新安装从道具传递的组件

时间:2020-10-09 12:50:06

标签: reactjs react-router react-router-dom

当将React与React Router一起使用时,我遇到了一些安装问题。 这甚至可能不是React Router本身的问题。 我想传递一些其他数据以及子路线。 这似乎是可行的,但是每次状态更改时,主页上的更改都会触发重新安装孙代。

这是为什么?为什么这只会发生在子孙身上而不仅仅是孩子?

代码示例:

import React, { useEffect, useState } from 'react';
import { Route, Switch,  BrowserRouter as Router, Redirect } from 'react-router-dom';

const MainPage = ({ ChildRoutes }) => {
  const [foo, setFoo] = useState(0);
  const [data, setData] = useState(0);
  const incrementFoo = () => setFoo(prev => prev + 1);

  useEffect(() =>{
    console.log("mount main")
  },[]);

  useEffect(() =>{
    setData(foo * 2)
  },[foo]);

  return (
    <div>
      <h1>Main Page</h1>      
      <p>data: {data}</p>
      <button onClick={incrementFoo}>Increment foo {foo}</button>
      <ChildRoutes foo={foo} />
    </div>
  );
};

const SecondPage = ({ ChildRoutes, foo }) => {
  const [bar, setBar] = useState(0);
  const incrementBar = () => setBar(prev => prev + 1);

  useEffect(() =>{
    console.log("mount second")
  },[]);

  return (
    <div>
      <h2>Second Page</h2>       
      <button onClick={incrementBar}>Increment bar</button>
      <ChildRoutes foo={foo} bar={bar} />
    </div>
  );
};

const ThirdPage = ({ foo, bar }) => {  
  useEffect(() =>{
    console.log("mount third")
  },[]);

  return (
    <div>
      <h3>Third Page</h3>
      <p>foo: {foo}</p>
      <p>bar: {bar}</p>
    </div>
  );
};

const routingConfig = [{
  path: '/main',
  component: MainPage,
  routes: [
    {
      path: '/main/second',
      component: SecondPage,
      routes: [
        {
          path: '/main/second/third',
          component: ThirdPage
        },
      ]
    }
  ]
}];

const Routing = ({ routes: passedRoutes, ...rest }) => {
  if (!passedRoutes) return null;

  return (
    <Switch>
      {passedRoutes.map(({ routes, component: Component, ...route }) => {
        return (
          <Route key={route.path} {...route}>
            <Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
          </Route>
        );
      })}
    </Switch>
  );
};

export const App = () => {
  return(
    <Router>
      <Routing routes={routingConfig}/>
      <Route exact path="/">
        <Redirect to="/main/second/third" /> 
      </Route>
    </Router>
  )
};


export default App;

MainPage中的每个单独状态更改都会导致ThirdPage重新安装。

由于React Router,我无法使用StackOverflow创建代码段。因此,这是一个与代码完全相同的codeandbox:https://codesandbox.io/s/summer-mountain-unpvr?file=/src/App.js

预期的行为是每个页面仅触发一次安装。 我知道我可能可以通过使用Redux或React.Context来解决此问题,但是现在我想知道是什么原因导致了这种现象,以及是否可以避免。

=========================

更新: 使用React.Context可以正常工作,但是我想知道如果没有它可以做到吗?

工作片:

const ChildRouteContext = React.createContext();

const ChildRoutesWrapper = props => {
  return (
    <ChildRouteContext.Consumer>
      { routes => <Routing routes={routes} {...props} /> }
    </ChildRouteContext.Consumer>    
  );
}

const Routing = ({ routes: passedRoutes, ...rest }) => {
  if (!passedRoutes) return null;

  return (
    <Switch>
      {passedRoutes.map(({ routes, component: Component, ...route }) => {
        return (
          <Route key={route.path} {...route}>
            <ChildRouteContext.Provider value={routes}>
              <Component {...rest} ChildRoutes={ChildRoutesWrapper}/>
            </ChildRouteContext.Provider>
          </Route>
        );
      })}
    </Switch>
  );
};

3 个答案:

答案 0 :(得分:5)

要了解此问题,我认为您可能需要了解React组件和React元素之间的区别以及React对帐的工作原理。

反应组件是基于类或功能的组件。您可以将其视为将接受一些道具和 最终返回一个React元素。而且您应该只创建一次React组件。

另一方面,

反应元素是描述组件实例或DOM节点及其所需属性的对象。 JSX提供 通过其React组件创建React元素的语法: <Component someProps={...} />

在单个时间点,您的React应用程序是一棵React元素树。最终,该树将转换为显示在屏幕上的实际DOM节点。

每次状态改变时,React都会构建另一棵新树。之后,React需要根据新树和最后一棵树之间的差异,找到一种有效更新DOM节点的方法。此过程称为对帐。此过程的差异算法是比较两个根元素,如果这两个是:

  • 不同类型的元素:React将拆除旧树并从头开始// this means re-mount that element (unmount and mount again)来构建新树。
  • 相同类型的DOM元素:React保留相同的底层DOM节点,并且仅更新更改的属性。
  • 相同类型的组成元素:React更新基础组件实例的道具以匹配新元素// this means keep the instance (React element) and update the props

这只是理论的简要介绍,让我们开始实践吧。

我将进行一个类比:React组件是特定工厂的工厂,而React元素是特定工厂的产品。工厂应该创建一次。

这行代码ChildRoutes是一个工厂,每次Component的父级重新渲染时(由于Javascript函数的创建方式),您都在创建一个新工厂:

<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>

MainPage基于routingConfig,创建了一个工厂来创建SecondPageSecondPage创建了一个工厂来创建ThirdPage。在MainPage中,当有状态更新时(例如:foo递增):

  1. MainPage重新呈现。它使用其SecondPage工厂创建一个SecondPage产品。由于工厂不变,因此稍后将根据“相同类型的组件元素”规则对创建的SecondPage产品进行差异化。
  2. SecondPage重新呈现(由于foo道具变更)。再次创建其ThirdPage工厂。因此,新创建的ThirdPage产品与以前的ThirdPage产品不同,并且后来根据“不同类型的元素”进行了区分。这就是导致ThirdPage元素重新挂载的原因。

要解决此问题,我正在使用渲染道具作为使用“一次创建”工厂的方式,以便稍后通过“相同类型的组件元素”来区分其创建的产品规则。

<Component 
    {...rest} 
    renderChildRoutes={(props) => (<Routing routes={routes} {...props} />)}
/>

这是工作示例:https://codesandbox.io/s/sad-microservice-k5ny0


参考:

答案 1 :(得分:3)

罪魁祸首是这条线:

<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>

更具体地说,是ChildRoutes道具。在每个渲染上,您正在为其提供一个全新的功能组件,因为给定了:

let a = props => <Routing routes={routes} {...props}/>
let b = props => <Routing routes={routes} {...props}/>

a === b总是以false结尾,因为它是2个不同的函数对象。由于您在每个渲染器上都为其提供了一个新的功能对象(一个新的功能组件),因此它只能从此Node重新安装组件子树,因为它每次都是一个新组件。

解决方案是提前在渲染方法外部 中创建一次此功能组件,如下所示:

const ChildRoutesWrapper = props => <Routing routes={routes} {...props} />

...然后传递此单个功能组件:

<Component {...rest} ChildRoutes={ChildRoutesWrapper} />

答案 2 :(得分:1)

由于您正在使用component道具,因此每次都会重新安装组件。

从文档中引用:

当您使用component(而不是下面的renderchildren)时,路由器会使用React.createElement根据给定的组件创建一个新的React元素。这意味着,如果您向组件prop提供内联函数,则将在每个渲染中创建一个新组件。这将导致现有组件的卸载和新组件的安装,而不仅仅是更新现有组件。在将内联函数用于内联渲染时,请使用renderchildren道具(如下)。

您可能需要的解决方案是编辑Routing组件以使用render而不是children

相关问题