ReactJS: Dynamically add a component on click

20,320

Below I've knocked up a really simple Parent / Child type setup,..

The Parent is responsible for rendering the Buttons, I just used a simple numbered array here. When you click any of the buttons, it calls the setState in the Parent, and this in turns causes the Parent to re-render it's Children.

Note: I've also used React Hooks to do this, I just find them more natural and easier to use. You can use Classes, the same principle applies.

const {useState} = React;

function Child(props) {
  const {caption} = props;
  const {lines, setLines} = props.pstate;
  return <button onClick={() => {
    setLines([...lines, lines.length]);
  }}>
    {caption}
  </button>;
}

function Parent(props) {
  const [lines, setLines] = useState([0]);  
  return lines.map(m => <Child key={m} caption={`Click ${m}`} pstate={{lines, setLines}}/>);
}


ReactDOM.render(<React.Fragment>
  <Parent/>
</React.Fragment>, document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="mount"></div>

Share:
20,320
devamat
Author by

devamat

Updated on May 02, 2020

Comments

  • devamat
    devamat about 4 years

    I have a menu button that when pressed has to add a new component. It seems to work (if I manually call the function to add the components they are shown). The problem is that if I click the button they are not shown, and I suppose because I should use setState to redraw them. I am not sure how to call the setState of another component within another function/component.

    This is my index.js

    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import Menu from './Menu';
    import * as serviceWorker from './serviceWorker';
    import Blocks from './Block.js';
    
    
    ReactDOM.render(
        <div className="Main-container">
            <Menu />
            <Blocks />
        </div>
        , document.getElementById('root'));
    
    // If you want your app to work offline and load faster, you can change
    // unregister() to register() below. Note this comes with some pitfalls.
    // Learn more about service workers:
    serviceWorker.unregister();
    

    Then I have the Menu.js

    import React from 'react';
    import './Menu.css';
    import {blocksHandler} from './Block.js';
    
    class Menu extends React.Component {
    
      constructor(props) {
    
        super(props);
        this.state = {value: ''};
    
        this.handleAdd = this.handleAdd.bind(this);
    
      }
    
      handleAdd(event) {
        blocksHandler.add('lol');
        console.log(blocksHandler.render());
      }
    
      render() {
        return (
          <div className="Menu">
            <header className="Menu-header">
              <button className="Menu-button" onClick={this.handleAdd}>Add block</button>
            </header>
          </div>
        );
      }
    }
    
    export default Menu;
    

    And finally the Block.js

    import React from 'react';
    import './Block.css';
    
    // this function adds components to an array and returns them
    
    let blocksHandler = (function() {
        let blocks = [];
        return {
            add: function(block) {
                blocks.push(block);
            },
            render: function() {
                return blocks;
            }
        }
    })();
    
    class Block extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                title: '',
                content: ''
            };
    
            this.handleChange = this.handleChange.bind(this);
            this.handleSubmit = this.handleSubmit.bind(this);
        }
    
        handleChange(event) {
            this.setState({[event.target.name]: event.target.value});
        }
    
        handleSubmit(event) {
            alert('A name was submitted: ' + this.state.title);
            event.preventDefault();
        }
    
        render() {
          return (
            <div className="Block-container">
                <form onSubmit={this.handleSubmit}>
                <div className="Block-title">
                    <label>
                        Block title:
                        <input type="text" name="title" value={this.state.value} onChange={this.handleChange} />
                    </label>
                </div>
                <div className="Block-content">
                    <label>
                        Block content:
                        <input type="text" name="content" value={this.state.value} onChange={this.handleChange} />
                    </label>
                </div>
                <input type="submit" value="Save" />
                </form>
            </div>
          );
        }
    }
    
    class Blocks extends React.Component {
    
        render() {
            return (
                <div>
                    {blocksHandler.render().map(i => (
                        <Block key={i} />
                    ))}
                </div>
            )
        }
    }
    
    
    export default Blocks;
    export {blocksHandler};
    

    I am a React complete beginner so I'm not even sure my approach is correct. Thank you for any help you can provide.

    • Keith
      Keith about 5 years
      You can pass parent state down to children using props,. If passing down props to children contains lots of sub-components, you can also pass props using contexts. reactjs.org/docs/context.html The best solution is not using React state at all, and using a more robust state management system, Redux is meant to be good for this.
    • devamat
      devamat about 5 years
      Thanks for your post. I'm trying to learn ReactJS and adding Redux might make things too complex for now. I'll check out your link.
    • Keith
      Keith about 5 years
      Yes, using Redux might confuse things if your just learning,.. I could maybe knock you up a really simple snippet that might help.
  • devamat
    devamat about 5 years
    Thank you for your post. I could not make this code work. I get an error at blocksArray: this.state.blocksArray.push(block), I changed it to blocksArray: this.state.blocksArray.push('lol') but with no luck, I still get an error: TypeError: this.state.blocksArray.map is not a function. Probably the array is empty for some reason.
  • Keith
    Keith about 5 years
    @devamat Your welcome, if anything in the above looks confusing, just ask away. If you want a little exercise on the above snippet, see if you can alter it so that only the last button is enabled each time.
  • Rajesh Kumaran
    Rajesh Kumaran about 5 years
    actually, inside handleAdd() , you need to give the configuration for the blocks,like this.state.blocksArray.push(/*give the data needed to render Block component*/)