【问题标题】:React calls constructor on every update for component within functional componentReact 在功能组件中的组件的每次更新时调用构造函数
【发布时间】:2019-10-23 08:26:27
【问题描述】:

我做了一个演示,显示了我的问题。在我的实际情况中,我有更复杂的数据结构,并且有更多需要它的理由。

原因 - 我制作了一个非常通用的组件包装器(示例中为App)。消费者程序员可以提供自己的方式,用于如何在我的组件中显示内容。他必须提供一个函数,该函数决定何时渲染什么(以AppmyComponentSwitch() 上的componentSwitch 属性为例)。他提供的所有组件都必须具有继承特定接口的道具(示例中为IItemBaseProps)。当然,也有一些情况,他需要为组件提供更多的 props。在那种情况下,我这样编写组件开关:

c = (props: IItemBaseProps) => <TypeB {...props} color="red" />;

这实现了我想要的 - c 将成为一个组件,它将 IItemBaseProps 作为道具。但它会返回 TypeB 组件并添加 color 属性。

出现的问题 - 每次渲染此功能组件时,都会调用 TypeB 的构造函数。 componentDidUpdate 永远不会被调用。

我制作了一个示例应用程序。代码如下:

import React, { Component } from "react";
import { render } from "react-dom";

interface IItemBaseProps {
  value: string;
}
interface IItemAProps extends IItemBaseProps {}
interface IItemBProps extends IItemBaseProps {
  color: string;
}

interface IItem {
  type: "A" | "B";
  props: IItemBaseProps;
}

class TypeA extends Component<IItemAProps> {
  constructor(props: IItemAProps) {
    super(props);
    console.log("TypeA::constructor", props);
  }
  componentDidUpdate(props: IItemAProps) {
    console.log("TypeA::componentDidUpdate", props, this.props);
  }
  render() {
    return (
      <div>
        <b>{this.props.value}</b>
      </div>
    );
  }
}
class TypeB extends Component<IItemBProps> {
  constructor(props: IItemBProps) {
    super(props);
    console.log("TypeB::constructor", props);
  }
  componentDidUpdate(props: IItemBProps) {
    console.log("TypeB::componentDidUpdate", props, this.props);
  }
  render() {
    return (
      <div>
        <u style={{ color: this.props.color }}>{this.props.value}</u>
      </div>
    );
  }
}
interface IITemWrapperProps {
  props: IItemBaseProps;
  component: React.ComponentType<IItemBaseProps>;
}
class ItemWrapper extends Component<IITemWrapperProps> {
  render() {
    return <this.props.component {...this.props.props} />;
  }
}

interface AppProps {
  componentSwitch: (type: "A" | "B") => React.ComponentType<IItemBaseProps>;
}
interface AppState {
  items: IItem[];
}

class App extends Component<AppProps, AppState> {
  constructor(props) {
    super(props);
    this.state = {
      items: [
        {
          type: "A",
          props: {
            value: "Value A1"
          }
        },
        {
          type: "A",
          props: {
            value: "Value A2"
          }
        },
        {
          type: "B",
          props: {
            value: "Value B1"
          }
        },
        {
          type: "ERR",
          props: {
            value: "Erroring item"
          }
        }
      ]
    };
  }

  onClick = () => {
    console.log("---- click");
    this.setState(state => {
      return {
        ...state,
        items: state.items.map((item, i) => {
          if (i === 0) {
            console.log("Updating items[" + i + "], type: " + item.type);
            return {
              ...item,
              props: {
                ...item.props,
                value: item.props.value + "!"
              }
            };
          }
          return item;
        })
      };
    });
  };

  render() {
    return (
      <div>
        <div>
          {this.state.items.map((item, i) => {
            return <ItemWrapper key={i} component={this.props.componentSwitch(item.type)} props={item.props} />;
          })}
        </div>
        <div>
          <button onClick={this.onClick}>Update first item!</button>
        </div>
      </div>
    );
  }
}

function myComponentSwitch(
  type: "A" | "B"
): React.ComponentType<IItemBaseProps> {
  if (type === "A") {
    return TypeA;
  }
  if (type === "B") {
    return (props: IItemBaseProps) => <TypeB {...props} color="red" />;
  }
  return (props: IItemBaseProps) => (
    <div>
      <i>{props.value}</i>
    </div>
  );
}

render(<App componentSwitch={myComponentSwitch}/>, 
document.getElementById("root"));

此外,您还可以在此处进行演示: https://stackblitz.com/edit/react-ts-a95cd6

有一个按钮,用于更新第一项。 控制台输出:

TypeA::constructor {value: "Value A1"}
TypeA::constructor {value: "Value A2"}
TypeB::constructor {value: "Value B1", color: "red"} // So far so good, 1st render
---- click // event on click
Updating items[0], type: A
TypeB::constructor {value: "Value B1", color: "red"} // TypeB is constructed again as a new instance
TypeA::componentDidUpdate {value: "Value A1"} {value: "Value A1!"} // TypeA components are just updated
TypeA::componentDidUpdate {value: "Value A2"} {value: "Value A2"}

我有点理解,为什么它会这样。我想知道 - 这是一个性能漏洞吗?我猜想,更新组件会比每次更新都制作新组件便宜。我该怎么做才能让这些组件只获得更新?

【问题讨论】:

    标签: javascript reactjs typescript


    【解决方案1】:

    每次调用 setState 时,组件都会保持该状态,并且他的所有子组件都会获得一个完全重新实例化的循环。

    如果您想优化循环,您可以重新修改应用程序以将状态部分声明到应该真正重新渲染的位置。

    或者使用全局状态解决方案(redux、easy-peasy ...)

    但是 react 团队移动和推广的方式是组件中的碎片化状态,并使用上下文来获得更多可共享的状态。

    【讨论】:

      【解决方案2】:

      重新创建 TypeB 组件的原因是 this.props.componentSwitch(item.type) 是在每次 App 组件重新渲染时计算的。虽然它的返回值在语义上是相等的,但它们是不同的 js 对象,从 reactJs 的角度来看,它们被视为不同的 prop 值。所以,ItemWrapper 组件认为它的 prop.component 发生了什么变化并重新创建它的子组件。

      会导致性能问题吗?如果你的构造函数做了一些繁重的工作,但通常你不会在那里放太多逻辑。

      但可能还有其他问题,例如在使用某些内部状态时。比如说,TypeB 组件在初始化时从 api 获取一些数据,然后显示出来。除非需要,否则您不希望调用 api,但是如果您在每次父级重新渲染时重新创建一个组件,最终的 api 调用将超出您的需要。

      这里可以做些什么来防止这些更新? this.props.componentSwitch(item.type) 结果可以包装成类似useMemo() 的东西,这样您就可以控制该值何时真正更新。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-05-18
        • 2021-08-16
        • 1970-01-01
        • 2022-11-22
        • 2019-12-02
        • 2018-08-18
        • 2021-08-24
        • 1970-01-01
        相关资源
        最近更新 更多