React 防止重新安装从 props 传递的组件

Kev*_*vin 6 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;
Run Code Online (Sandbox Code Playgroud)

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

由于 React Router,我无法使用 StackOverflow 创建代码段。所以这是一个具有完全相同代码的代码和框: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>
  );
};
Run Code Online (Sandbox Code Playgroud)

don*_*han 6

要理解这个问题,我认为您可能需要了解 React 组件和 React 元素之间的区别以及 React 协调是如何工作的。

React 组件要么是基于类的组件,要么是基于函数的组件。你可以把它想象成一个接受一些 props 并最终返回一个 React 元素的函数。你应该只创建一次 React 组件。

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

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

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

  • 不同类型的元素: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}/>}/>
Run Code Online (Sandbox Code Playgroud)

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

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

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

<Component 
    {...rest} 
    renderChildRoutes={(props) => (<Routing routes={routes} {...props} />)}
/>
Run Code Online (Sandbox Code Playgroud)

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


参考: