【问题标题】:React prevent remounting components passed from propsReact 防止重新安装从 props 传递的组件
【发布时间】:2021-01-24 13:25:08
【问题描述】:

将 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 创建 sn-p。所以这里有一个完全相同的代码框: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>
  );
};

【问题讨论】:

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


    【解决方案1】:

    要理解这个问题,我想你可能需要了解 React 组件和 React 元素之间的区别以及 React 协调的工作原理。

    React 组件 是基于类的组件或函数式组件。你可以把它想象成一个接受一些道具和 最终返回一个 React 元素。而且你应该只创建一次 React 组件。

    另一方面,

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

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

    每次状态改变时,React 都会构建另一个全新的树。之后,React 需要想办法根据新树和最后一棵树之间的差异来有效地更新 DOM 节点。这个过程称为协调。此过程的 diffing 算法是比较两个根元素时,如果这两个是:

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

    这是一个简单的理论,让我们开始实践吧。

    我打个比方:React 组件是一个工厂,React 元素是一个特定工厂的产品。工厂应该创建一次。

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

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

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

    1. MainPage 重新渲染。它使用其SecondPage 工厂来创建SecondPage 产品。由于它的工厂没有改变,因此创建的SecondPage 产品后来根据“相同类型的组件元素”规则进行了区分。
    2. SecondPage 重新渲染(由于 foo 道具更改)。再次创建其ThirdPage 工厂。所以新创建的ThirdPage 产品与之前的ThirdPage 产品不同,后来根据“不同类型的元素”进行了区分。这就是导致ThirdPage 元素被重新安装的原因。

    为了解决这个问题,我使用渲染道具作为使用“created-once”工厂的一种方式,以便其创建的产品稍后通过“相同类型的组件元素”进行区分规则。

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

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


    参考:

    【讨论】:

    • 哇,感谢您的详细回答。这也避免了对 React Context 的需求,并解释了为什么它只发生在 ThirdPage 上。我必须重读几遍才能理解它。但总的来说,可以肯定地说,当您需要传递一些数据时,最好使用与 renderChildProps 类似的函数而不是实际组件?
    • 嗨@Kevin,根据我的经验,只要您知道“工厂应该创建一次”规则,我会说使用渲染道具是安全的。为了进一步优化,在组件重新渲染的成本很高的情况下,您可能希望控制何时重新渲染组件,即React.memouseEffect 使用适当的依赖数组、useCallback、@ 987654351@,... 进来了。例如,在上面的演示中,您可以使用useCallback 来防止每次都创建新的renderChildRoutes 函数。
    【解决方案2】:

    罪魁祸首是这一行:

    <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 个不同的函数对象。由于你在每次渲染时都给它一个新的函数对象(一个新的函数组件),它别无选择,只能从此节点重新挂载组件子树,因为它每次都是一个新组件。

    解决方案是预先在您的渲染方法之外创建一次此功能组件,如下所示:

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

    ...然后传递这个单一的功能组件:

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

    【讨论】:

    • 我也试过了,但我不太清楚我该怎么做。由于路由也是传递给组件的,并且路由来自于map函数。
    【解决方案3】:

    您的组件每次都会重新安装,因为您使用的是 component 属性。

    引用自文档:

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

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

    【讨论】:

    • 当我改变孩子来渲染问题仍然是一样的
    猜你喜欢
    • 1970-01-01
    • 2016-01-14
    • 2019-09-11
    • 2018-04-06
    • 2020-10-07
    • 2019-09-21
    • 1970-01-01
    • 1970-01-01
    • 2017-11-17
    相关资源
    最近更新 更多