EviSvil
EviSvil

Reputation: 656

ReactJS - Infinite Loop calling Wrapped Method

I have the usual problem with infinite loop and I don't know why.

Im using reactJS 16.5.2

The loops generally occurs when you write a SetState where not allowed (for example in render method).

Im following this guide: https://medium.com/@baphemot/understanding-reactjs-component-life-cycle-823a640b3e8d to pay attention about this issue.

I made several HOC(Decorators/Wrapper) components to concentrate general purpose methods in one point using props to propagate them to every children.

It generally works perfectly.

I tried to simplify my components structure below.

The problem is the FORM and its children. One of the input has a DropDown that has to be populated with a method of the upper Wrapper. I put the call in componentDidMount(as the link above suggest). Unfortunally the wrapper setState seems to trigger a complete descrution and re-building of FORM Component. I put a console.log in every constructor from Wrapped to the form. Only the FORM and all its INPUTS are reacreated (and not updated).

This recreation generates an infinite loop because componentDidMountis triggered everytime.

I don't know how to fix this. I've checked every "key" properties and ALL components has their unique keys. I'm asking you WHY react recreate instead of update?

Is due to the form building method in parent render? And if so, which is the right design pattern to build a form with Async data population?

enter image description here

Upvotes: 0

Views: 327

Answers (2)

EviSvil
EviSvil

Reputation: 656

Thanking Matt Carlotta for his answer, I figure out what was the problem.

In the image above I simplified too much so I missed one important declaration.

In "FinalComponent" when I was creating the SomeFormComponent, due to its wrapping, I was doing something like this:

renderForm()
{
  var WrappedFormComponent = FormHOC(SomeFormComponent();
  return <WrappedFormComponent {...this.props} [...] />
}

It's obvious that with that syntax, the Form is instantatied every time due to renderForm method called in render method.

The solution is very simple. I moved that line above the component:

const WrappedFormComponent = FormHOC(SomeFormComponent();
export default class FinalComponent extends React.Component

Upvotes: 0

Matt Carlotta
Matt Carlotta

Reputation: 19782

Simplify your life and instead of creating a bunch of wrappers, just create a single container-component that'll function the same way. For example, you would create a container that cares about data and state, then shares it and its methods with a reusable child component (as shown below, both function the same).

This would work exactly the same way with data fetched from an API. You'll retrieve data in componentDidMount, set it state, then pass down the state to the reuseable component.

You can get super granular with your reusable components. For example a reusable button that's sole purpose is to submit a form. Or a reusable input that only captures numbers between 1 and 100 and so on.

If your components are heavily nested, then consider using redux.

Working example: https://codesandbox.io/s/x2ol8wmzrp

containers/Form.js (container-component)

import React, { Component } from "react";
import Fields from "../components/Fields";

export default class Form extends Component {
  state = {
    buttonFields: [
      { id: "Apples", quantity: 1 },
      { id: "Strawberries", quantity: 1 },
      { id: "Grapes", quantity: 1 },
      { id: "Apricots", quantity: 1 }
    ]
  };

  handleButtonClick = id => {
    this.setState(prevState => ({
      buttonFields: prevState.buttonFields.map(
        item =>
          id === item.id ? { id, quantity: item.quantity + 1 } : { ...item }
      )
    }));
  };

  render = () => (
    <Fields
      {...this.state}
      onButtonClick={this.handleButtonClick}
      title="Container Component"
    />
  );
}

components/Fields.js (reusable component)

import React from "react";

export default ({ buttonFields, onButtonClick, title }) => (
  <div className="container">
    <h1 style={{ textAlign: "center" }}>{title}</h1>
    {buttonFields.map(({ id, quantity }) => (
      <button
        style={{ marginRight: 10 }}
        className="uk-button uk-button-primary"
        key={id}
        onClick={() => onButtonClick(id)}
      >
        {id} ({quantity})
      </button>
    ))}
  </div>
);

containers/Wrapper.js (unnecessary wrapper)

import React, { Component } from "react";

export default WrappedComponent => {
  class Form extends Component {
    state = {
      buttonFields: [
        { id: "Apples", quantity: 1 },
        { id: "Strawberries", quantity: 1 },
        { id: "Grapes", quantity: 1 },
        { id: "Apricots", quantity: 1 }
      ]
    };

    handleButtonClick = id => {
      this.setState(prevState => ({
        buttonFields: prevState.buttonFields.map(
          item =>
            id === item.id ? { id, quantity: item.quantity + 1 } : { ...item }
        )
      }));
    };

    render = () => (
      <WrappedComponent
        {...this.state}
        onButtonClick={this.handleButtonClick}
        title="Wrapper"
      />
    );
  }

  return Form;
};

Upvotes: 2

Related Questions