alpakyol
alpakyol

Reputation: 2459

ReactJS: How can I change dynamically inserted Component's data

I'm trying to change children Component to another component by using state. This injects new Component correctly, however, if I want to change its props dynamically, nothing is changing. componentWillReceiveProps isn't triggered.

In my scenario, I'll have many components like TestComponent (nearly 20-30 components) and they all have different HTML layout (also they have sub components, too). I switch between those components by selecting some value from some list.

Loading all those components initially doesn't seem a good idea I think. On the other hand, I haven't found anything about injecting a Component inside main Component dynamically.

Here is a very basic example of what I want to achieve. When clicking on the button, I insert TestComponent inside App. After that, on every one second, I increment a state value which I try to bind TestComponent but, the component value is not updating.

If I use commented snippet inside setInterval function instead of uncommented, it works but I have to write 20-30 switch case for finding the right component in my real code (which I also wrote when selecting a value from list) so, I want to avoid using that. Also, I'm not sure about the performance.

So, is this the correct approach, if so, how can I solve this problem? If it is wrong, what else can I try?

class App extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {
      component: <p>Initial div</p>,
      componentData: 0
    };
    
    this.onClickHandler = this.onClickHandler.bind(this);
  }
  
  onClickHandler = () => {
    this.setState({
      component: <TestComponent currentValue={this.state.componentData} />
    });
    
    setInterval(() => {
      this.setState({
        componentData: this.state.componentData + 1
      })
      
      // This will update TestComponent if used instead of above
      /*this.setState({
        componentData: this.state.componentData + 1,
        component: <TestComponent currentValue={this.state.componentData} />
      });*/
    }, 1000)
  }
  
  render() {
    return(
      <div>
        <h4>Click the button</h4>
        <button onClick={this.onClickHandler}>Change Component</button>
        {this.state.component}
      </div>
    )
  }
}

class TestComponent extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {
      currentValue: this.props.currentValue
    };
  }
  
  componentWillReceiveProps(nextProps) {
    this.setState({
      currentValue: nextProps.currentValue
    });
  }
  
  render() {
    return (
      <p>Current value: {this.state.currentValue}</p>
    )
  }
}

ReactDOM.render(
  <App />
  ,document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

<div id="app" style="width: 200px; height: 200px;"></div>

Upvotes: 1

Views: 6978

Answers (2)

mindaJalaj
mindaJalaj

Reputation: 448

To dynamically render the child components you can use React.createElement method in parent, which results in invoking different components, this can be used as, below is sample code, hope it helps.

getChildComponent = (childComponentName) => {
        const childComponents = {
                TestComponent1,
                TestComponent2,
                TestComponent3,
                TestComponent4
            },
            componentProps = Object.assign({}, this.props,this.state, {
                styles: undefined
            });
        if (childComponents[childComponentName]) {
            return React.createElement(
                                        childComponents[childComponentName], 
                                        componentProps);
        }
        return null;
    }

render(){
    this.getChildComponents(this.state.childComponentName);
}

Here in the render function, pass the component name, and child will render dynalicaaly. Other way of doing this can be, make childComponents object as array , look below fora sample

const childComponents = [
                    TestComponent1,
                    TestComponent2,
                    TestComponent3,
                    TestComponent4
                ]

Note: You have to import all child components here in parent, these are not strings.

Upvotes: 1

Jose Paredes
Jose Paredes

Reputation: 4090

That's because as Facebook mentions in their React documentation.

When you call setState(), React merges the object you provide into the current state.

The merging is shallow

For further information read the documentation

So for this case the only modified value will be componentData and component won't trigger any updates

Solution

A better case to solve this issue is using Higher-Order components (HOC) so the App component doesn't care which component you are trying to render instead It just receives a component as a prop so you can pass props to this component base on the App state.

Also, you don't need a state in TestComponent since you get the value as a prop and it's handled by App.

I also added a condition to prevent adding multiples setInterval

class App extends React.Component {
  interval;

  constructor(props) {
    super(props);
    
    this.state = {
      componentData: 0
    };

    this.onClickHandler = this.onClickHandler.bind(this);
  }
  
  onClickHandler = () => {
    if (!this.interval) {
      this.setState({
        componentData: this.state.componentData + 1
      });

      this.interval = setInterval(() => {
        this.setState({
          componentData: this.state.componentData + 1
        });
      }, 1000);
    }
  }
  
  render() {
    let Timer = this.props.timer;

    return(
      <div>
        <h4>Click the button</h4>
        <button onClick={this.onClickHandler}>Change Component</button>
        {!this.state.componentData ? <p>Initial div</p> : <Timer currentValue={this.state.componentData} />}
      </div>
    )
  }
}

class TestComponent extends React.Component {
  render() {
    const { currentValue } = this.props;

    return (
      <p>Current value: {currentValue}</p>
    )
  }
}

ReactDOM.render(<App timer={TestComponent} /> ,document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.js"></script>

<div id="app" style="width: 200px; height: 200px;"></div>

Upvotes: 1

Related Questions