13

I have a big component <User> and a small one <Task> and want to give a big component a button and on click on it append the component <Task> to the <User>

User.jsx

<div>
  <Task />
  <button onClick={`Function here to append a new Task component`}>
    New Task
  </button>
</div>

Task.jsx

<div>
  This is New task
</div>
vsync
  • 118,978
  • 58
  • 307
  • 400
Zeyad Etman
  • 2,250
  • 5
  • 25
  • 42
  • Could you include both your `User` and `Task` component code, and show what you have tried so far? – Tholle Jul 18 '18 at 14:17
  • I don't know how to do this in reactjs – Zeyad Etman Jul 18 '18 at 14:18
  • write the component here or in https://jsfiddle.net/ it will help us help you – hod caspi Jul 18 '18 at 14:20
  • @ZeyadEtman check this answer, you will get a basic idea about conditional rendering of the component [**Link**](https://stackoverflow.com/questions/47978993/how-to-render-element-on-click-of-button-reactjs/47979053#47979053), in case you want to render multiple component, maintain a count (initial value=0) in state variable and use loop to render that many elements. – Mayank Shukla Jul 18 '18 at 14:21
  • You should adapt a different mindset when working with React. React allows you to describe your UIs declaratively. Think about it as "given this data, this it what my UI look like". So instead of trying to "append a new Task component", you'd update the data the UI is based on, which then would render as many Task components as needed. – Felix Kling Jul 18 '18 at 14:30
  • Linked: https://stackoverflow.com/q/57455821/104380 – vsync Aug 12 '19 at 09:19

4 Answers4

19

You should let React render everything, and your job is only to tell React what to render and, in your case, how many times. For that, a counter can be used to track how many dynamically-added should be "injected" inside the <Example> component.

A mindshift is needed here, since in your example you are coming from a place where you think that the click handler itself should modify the DOM, and in React that is an antipattern.

Instead, you should work with state, and that means the click handler should update the state of the host component and that will trigger a re-render (this is how React works) and in the next render cycle, your added component will be rendered as many times as the counter value, because that counter change is what triggered the re-rendering.

In React, the props & state are the way to trigger re-render and any DOM modification should be done by changing the internal component's state, or by sending different props, from the parent component.


In the below example I do not use Classes, but use Hooks instead, because I've stopped using class altogether once hooks were released, because I think it's cleaner:

const {useState, Fragment} = React

// The added element component
const AddedElement = () => <p><input placeholder='text box' /></p>

// The parent component
const App = () => {
  const [count, setCount] = useState(0) // Name it however you wish

  return <Fragment>
    <button onClick={() => setCount(count + 1)}>Click me</button>
    { [...Array(count)].map((_, i) => <AddedElement key={i} />) }
  </Fragment>
}

// Render 
ReactDOM.render(<App />, document.getElementById("react"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.11.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
<div id="react"></div>

If you are interested in how to repeatedly render the same component, I've written an answer about this in this question: How can I render repeating React elements?

vsync
  • 118,978
  • 58
  • 307
  • 400
  • Use memos in react – Sabesan Dec 03 '19 at 21:15
  • 1
    How will you keep track of the value of each new element? – Faahmed Jun 23 '20 at 15:05
  • @Faahmed - that is the easiest part actually. Every app has its own Form-state system. I cannot give a solution that can apply for all and not even 10% I would guess, but, since you have an index for each input, you can store the value (per field) in another key/value object state – vsync Jun 01 '23 at 08:20
7

You could do something like the following.

const User = () => {
  return <p>User</p>
}

class App extends React.Component {

  state = {
    users: []
  }

  addUser = () => {
    this.setState({
      users: [...this.state.users, <User />]
    })
  }

  render() {
    return (
      <div>
        <button onClick={this.addUser}>Add User</button>
        {this.state.users}
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<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="root"></div>

There is a code sandbox here.

Paul Fitzgerald
  • 11,770
  • 4
  • 42
  • 54
1

const Task = (props) => {
  return (
    <li>{props.value}</li>
  )
}

class TaskForm extends React.Component {
  state = { 
    value : ''
  };
  
  handleChange = (e) => {
    const { value } = e.target;
    this.setState(_ => ({
      value
    }));
  }
  
  handleAdd = (e) => {
    const { value } = this.state;
    if (value === '') { return; }
    this.props.onNewTask(value);
    this.setState(_ => ({
      value : ''
    }))
  }
  
  render () {
    const { value } = this.state;
    return (
      <div>
        <input 
          value={value} 
          onChange={this.handleChange}
        />
        <button onClick={this.handleAdd}>Add</button>
      </div>
    )
  }
}

class User extends React.Component {
  state = {
    tasks : []
  }
  
  handleNewTask = (task) => {
    this.setState(state => ({
      ...state,
      tasks : [
        ...state.tasks,
        task
      ]
    }))
  }
  
  renderTasks () {
    const tasks = this.state.tasks.map(t => (
            <Task key={t} value={t} />
    ));
    return (
      <ul>
        {tasks}
      </ul>
    )
  }
  
  render () {
    return (
      <div>
        <TaskForm onNewTask={this.handleNewTask}/>
        {this.renderTasks()}
      </div>
    )
  }
}


ReactDOM.render(<User/>, document.querySelector('#app'));
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.1/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.1/umd/react-dom.production.min.js"></script>
</head>
<body>
 <div id="app"></div>
</body>
</html>
AngelSalazar
  • 3,080
  • 1
  • 16
  • 22
1
import React from "react";
import "./styles.css";

const Element = () => {
  return (
    <div style={{ marginTop: "10px" }}>
      <input type="file" />
      <select id="cars" name="carlist" form="carform">
        <option value="volvo">Volvo</option>
        <option value="saab">Saab</option>
        <option value="opel">Opel</option>
        <option value="audi">Audi</option>
      </select>
    </div>
  );
};

class App extends React.Component {
  state = {
    elements: [
      <div style={{ marginTop: "10px" }}>
        <input type="file" />
        <select id="cars" name="carlist" form="carform">
          <option value="volvo">Volvo</option>
          <option value="saab">Saab</option>
          <option value="opel">Opel</option>
          <option value="audi">Audi</option>
        </select>
      </div>
    ]
  };

  addUser = () => {
    this.setState({
      elements: [...this.state.elements, <Element />]
    });
  };

  render() {
    return (
      <div className="App">
        <h3>Append a React component in another on button click</h3>
        {this.state.elements}

        <button onClick={this.addUser} style={{ marginTop: "10px" }}>
          Add User
        </button>
      </div>
    );
  }
}

export default App;