简体   繁体   中英

React calls constructor on every update for component within functional component

I made a demo, which shows my problem. In my actual case, I have much more complicated data structures, and more reason for why I need this.

Reason for this - I have made a quite generic component wrapper ( App in the example). Consumer programmer can provide his own ways, for how to display the content in my Component. He has to provide a function, which decides - what to render when ( componentSwitch prop on App and myComponentSwitch() as an example). All of the Components that he provides, has to have props, which inherit a specific interface ( IItemBaseProps in the example). Of course, there are situations, where he needs to provide more props for the component. In that case, I wrote the component switch like this:

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

This achieves what I want - c will be a component which will take IItemBaseProps as a props. But it will return TypeB component with addition color prop.

The problem that arises - every time this functional component is rendered, constructor for TypeB is called. componentDidUpdate is never called.

I have made an example app. Here is the code:

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

And also, you can demo it here: https://stackblitz.com/edit/react-ts-a95cd6

There is a button, that updates 1st item. The output in console:

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"}

I kind of understand, why it behaves like this. I was wondering - is this a performance hole? I would guess, that updating components would be cheaper than making new ones on every update. How would I do, so that those components gets only an updates?

Each time you call setState the component that holds that state, and all his children get a fully reInstanciate cycle.

If you want to optimice the cycle, you can re tinker the app to have the portion of state declared to the places that should truly reRender.

Or use aa global state solution (redux, easy-peasy …)

But the way that react team is moving and promote is fragmented states in components, and useContext for more shareable state.

Reason of recreating a TypeB component is that this.props.componentSwitch(item.type) is calculated on every App component re-render. While its return value is semantically equal, these are different js objects and are treated as different prop values from reactJs perspective. So, ItemWrapper component thinks what its prop.component changed and recreates its child component.

Could it lead to performance issues? It might in case your constructor does some heavy lifting, but usually you don't put much logic there.

There could be other problems though, for example when using some internal state. Let's say, for example, TypeB component fetches some data from api when initialized and then displays it. You don't want to call api unless it is needed, but if you recreate a component on every parent re-render, you end up with more api calls than you need.

What could be done here to prevent those updates? this.props.componentSwitch(item.type) result could be wrapped into something like useMemo() , so that you get control on when this value really gets updated.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM