devamat
devamat

Reputation: 2503

ReactJS: array of compoents issue

I have a Main.js page that has one button: when you click it it adds a Block component to an array and to the page. You can add as many Block components as you want. Each Block component has a "delete" button, that will remove the Block from the array and from the page.

Menu.js:

import React from 'react';
import './Menu.css';
import Block from './Block.js';
import './Block.css';

export default class Menu extends React.Component {

  constructor(props) {

    super(props);

    this.state = { value: '', blocksArray: [] };

    this.addBlock = this.addBlock.bind(this);
    this.removeBlock = this.removeBlock.bind(this);

    this.blocks = [];

  }

  addBlock() {

    this.blocks.push({ title: 'Section title' + this.blocks.length, content: 'Content' + this.blocks.length });
    this.setState({ value: '', blocksArray: this.blocks });
  }

  removeBlock(index) {
    this.blocks.splice(index, 1);
    this.setState({ value: '', blocksArray: this.blocks })
  }

  renderBlocks = () => {
    return (
      this.state.blocksArray.map((block, index) =>
      <Block
        remove={() => this.removeBlock(index)} 
        key={index}
        title={block.title}
        content={block.content}
      />  
      )
    )
  }

  render() {
    return (
      <div>
        <div className="Menu">
          <header className="Menu-header">
            <button className="Menu-button" onClick={ () => this.addBlock() }>Add block</button>
          </header>
        </div>
        <div>
          { this.renderBlocks() }
        </div>
      </div>
    );
  }
}

Block.js (version 1)

import React from 'react';
import './Block.css';

class Block extends React.Component {
    constructor(props) {

        super(props);

        this.state = {
            title: props.title,
            content: props.content,
            remove: props.remove
        };

        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>
                        Title: 
                        <input type="text" name="title" value={this.props.title} onChange={this.handleChange} />
                    </label>
                </div>
                <div className="Block-content">
                    <label>
                        Content: 
                        <input type="text" name="content" value={this.props.content} onChange={this.handleChange} />
                    </label>
                </div>
                <input type="submit" value="Save"  />
                <input type="button" value="Delete" onClick= { () => this.state.remove() } />
            </form>
        </div>
      );
    }
  }

export default Block;

The issue: I found myself stuck with 2 situations and neither works properly.

First non working solution for Block.js:

<input type="text" name="title" value={this.props.title} onChange={this.handleChange} />

<input type="text" name="content" value={this.props.content} onChange={this.handleChange} />

If I use value={this.props.content} and value={this.props.title} when I push the delete button on the Block it works but I can't edit the text in the field since its value is always retrieved from the props.

Second non working solution for Block.js:

<input type="text" name="title" value={this.state.title} onChange={this.handleChange} />

<input type="text" name="content" value={this.state.content} onChange={this.handleChange} />

If I use value={this.state.content} and value={this.state.title} I can edit the text fields and when I push the delete button on the Block it removes properly the component from the array, but the text displayed in the fields is wrong (it's as if it's always popping only the last component from the array). Let me explain with a few screenshots.

Let's say I added 4 Block components, as follow:

enter image description here

Then I click on the delete button of the Block with "Section title1" / "Content1", as this screenshot:

enter image description here

It apparently removes the right element in the array, but for some reason I get the wrong text in the component:

Array console.log:

0: Object { title: "Section title0", content: "Content0" }
1: Object { title: "Section title2", content: "Content2" }
2: Object { title: "Section title3", content: "Content3" }

Displayed text:

enter image description here

I'm obviously missing something and I have been stuck for a while. Can someone explain what is wrong?

Upvotes: 0

Views: 53

Answers (2)

Chris B.
Chris B.

Reputation: 5763

Your change handler needs to be operating on the state in the parent component where the title/content are coming from. The values shown in the Blocks are being read from the Menu's state, so while editing the block data changes its own internal state, the values coming from the menu to the block as props are staying the same because the internal state is not being fed back.

You could write a function to edit the arrays in the Menu state in place:

this.editBlock = this.editBlock.bind(this);

...

  editBlock(index, newBlock) {
    let blocks = Array.from(this.state.blocksArray);
    blocks[index] = newBlock;
    this.setState({
      blocksArray: blocks
    })
  }

and then pass that to the Block as props and call it when the change event fires:

      <Block
        remove={() => this.removeBlock(index)} 
        key={index}
        title={block.title}
        content={block.content}
        index={index}
        editBlock={this.editBlock}
      />  
    handleChange(event) {
        this.setState({[event.target.name]: event.target.value}, () => {
          this.props.editBlock(this.props.index, { title: this.state.title, content: this.state.content})
        });
    }

Working demo here.

Upvotes: 1

carter108
carter108

Reputation: 71

I think the problem is you are setting index as the key for each Block. Origin keys are [0, 1, 2, 3]. When you remove Section title1, new render will produce keys [0, 1, 2]. So React assumes that element with keys [0, 1, 2] are not changed and key 3 is removed. So it removed the last one. Try to use an unique property for the key. You can read more here: https://reactjs.org/docs/reconciliation.html#keys

Upvotes: 2

Related Questions