1

I'm working on a React.js based outliner (in similar vein to workflowy). I'm stuck at a point. When I press enter on any one item, I want to insert another item just below it.

So I have used setState function to insert an item just below the current item as shown by the code snippet below:

 this.setState({
      items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
 })

However, the app is re-rendering as blank without any error showing up.

My full source so far:

import './App.css';

import React, { Component } from 'react';
import ContentEditable from 'react-contenteditable';

const items = [
  {
    _id: 0,
    content: 'Item 1 something',
    note: 'Some note for item 1'
  },
  {
    _id: 5,
    content: 'Item 1.1 something',
    note: 'Some note for item 1.1'
  },
  {
    _id: 1,
    content: 'Item 2 something',
    note: 'Some note for item 2',
    subItems: [
      {
        _id: 2,
        content: 'Sub Item 1 something',
        subItems: [{
          _id: 3,
          content: 'Sub Sub Item 4'
        }]
      }
    ]
  }
];

class App extends Component {
  render() {
    return (
      <div className="App">
        <Page items={items} />
      </div>
    );
  }
}

class Page extends Component {
  constructor(props) {
    super(props);

    this.state = {
      items: this.props.items
    };

    this.insertItem = this.insertItem.bind(this);
  }

  render() {
    return (
      <div className="page">
        {
          this.state.items.map(item =>
            <Item _id={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
          )
        }
      </div>
    );
  }

  insertItem(itemID) {
    console.log(this.state.items);

    this.setState({
      items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
    })

    console.log(this.state.items);
  }
}

class Item extends Component {
  constructor(props) {
    super(props);

    this.state = {
      content: this.props.content,
      note: this.props.note
    };

    this.saveItem = this.saveItem.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
  }

  render() {
    return (
      <div key={this.props._id} className="item">
        <ContentEditable html={this.state.content} disabled={false} onChange={this.saveItem} onKeyPress={(event) => this.handleKeyPress(this.props._id, event)} />

        <div className="note">{this.state.note}</div>
        {
          this.props.subItems &&
          this.props.subItems.map(item =>
            <Item _id={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
          )
        }
      </div>
    );
  }

  saveItem(event) {
    this.setState({
      content: event.target.value
    });
  }

  handleKeyPress(itemID, event) {
    if (event.key === 'Enter') {
      event.preventDefault();
      console.log(itemID);
      console.log(event.key);
      this.props.insertItem(itemID);
    }
  }
}

export default App;

The github repo: https://github.com/Hirvesh/mneme

Can anybody help me understand as to why it's not rendering again after I update the items?

Edit:

I have update the code to add keys, as suggested below, still rendering as blank, despite the state updating:

import './App.css';

import React, { Component } from 'react';
import ContentEditable from 'react-contenteditable';

const items = [
  {
    _id: 0,
    content: 'Item 1 something',
    note: 'Some note for item 1'
  },
  {
    _id: 5,
    content: 'Item 1.1 something',
    note: 'Some note for item 1.1'
  },
  {
    _id: 1,
    content: 'Item 2 something',
    note: 'Some note for item 2',
    subItems: [
      {
        _id: 2,
        content: 'Sub Item 1 something',
        subItems: [{
          _id: 3,
          content: 'Sub Sub Item 4'
        }]
      }
    ]
  }
];

class App extends Component {
  render() {
    return (
      <div className="App">
        <Page items={items} />
      </div>
    );
  }
}

class Page extends Component {
  constructor(props) {
    super(props);

    this.state = {
      items: this.props.items
    };

    this.insertItem = this.insertItem.bind(this);
  }

  render() {
    return (
      <div className="page">
        {
          this.state.items.map(item =>
            <Item _id={item._id} key={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
          )
        }
      </div>
    );
  }

  insertItem(itemID) {
    console.log(this.state.items);

    this.setState({
      items: this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})
    })

    console.log(this.state.items);
  }
}

class Item extends Component {
  constructor(props) {
    super(props);

    this.state = {
      content: this.props.content,
      note: this.props.note
    };

    this.saveItem = this.saveItem.bind(this);
    this.handleKeyPress = this.handleKeyPress.bind(this);
  }

  render() {
    return (
      <div className="item">
        <ContentEditable html={this.state.content} disabled={false} onChange={this.saveItem} onKeyPress={(event) => this.handleKeyPress(this.props._id, event)} />

        <div className="note">{this.state.note}</div>
        {
          this.props.subItems &&
          this.props.subItems.map(item =>
            <Item _id={item._id} key={item._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
          )
        }
      </div>
    );
  }

  saveItem(event) {
    this.setState({
      content: event.target.value
    });
  }

  handleKeyPress(itemID, event) {
    if (event.key === 'Enter') {
      event.preventDefault();
      console.log(itemID);
      console.log(event.key);
      this.props.insertItem(itemID);
    }
  }
}

export default App;

Edit 2:

As per the suggestions below, added key={i._id}, still not working, pushed latest code to github if anybody wants to take a look.

  this.state.items.map((item, i) =>
    <Item _id={item._id} key={i._id} content={item.content} note={item.note} subItems={item.subItems} insertItem={this.insertItem} />
  )
JJJ
  • 32,902
  • 20
  • 89
  • 102
Hirvesh
  • 7,636
  • 15
  • 59
  • 72
  • 1
    It would be much easier to help if you format the code and make it more readable. You could also remove any unused code, if possible. – nem035 Feb 04 '17 at 09:24
  • Check the long unreadable code line separately: `const newItems = this.state.items.splice(this.state.items.map((item) => item._id).indexOf(itemID) + 1, 0, {_id: (new Date().getTime()), content: 'Some Note'})` `this.setState({items: newItems})` – webdeb Feb 04 '17 at 09:49
  • I did, the item state object updates just fine, the item is added, but the app re-renders as blank – Hirvesh Feb 04 '17 at 09:50
  • However, the first argument of `splice` is not array, so it cannot be right. – webdeb Feb 04 '17 at 09:50
  • Oh, I see there is indexOf at the end.. – webdeb Feb 04 '17 at 09:52
  • Possible duplicate of [How to force remounting on React components?](http://stackoverflow.com/questions/35792275/how-to-force-remounting-on-react-components) – Chris Feb 04 '17 at 09:54
  • You are setting the key prop wrong it should go into ` – webdeb Feb 04 '17 at 09:56
  • @webdeb I added the key as you requested. See my second edit. Still not working. I'm at a loss here. – Hirvesh Feb 04 '17 at 10:00
  • @the_archer see my answer... your setState is setting `[]` to items. – webdeb Feb 04 '17 at 10:25

2 Answers2

3

You miss to put a key on Item, because of that React could not identify if your state change.

<div className="page">
{
  this.state.items.map((item, i) =>
     <Item _id={item._id} 
      key={i} // <---- HERE!!
      content={item.content} 
      note={item.note} 
      subItems={item.subItems} 
      insertItem={this.insertItem} />     
  )}
</div>
damianfabian
  • 1,681
  • 12
  • 16
1

There are multiple issues with your code:

  1. in you are passing this.insertItem into SubItems (which does not exists)
  2. your this.state.items.splice, changes this.state.items, but returns []. So your new State is []

try this:

this.state.items.splice(/* your splice argument here */)
this.setState({ items: this.state.items }, () => {
  // this.setState is async! so this is the proper way to check it.
  console.log(this.state.items);
});

To accomplish what you actually want, this will not be enough.. But at least, when you have fixed it, you can see some results.

webdeb
  • 12,993
  • 5
  • 28
  • 44
  • thank you! I believe I'm in the right direction. Trying to get things right, will update in some :) – Hirvesh Feb 04 '17 at 10:26
  • Any idea why the splice is not working? I can't understand why it's returning an empty array on splice... – Hirvesh Feb 04 '17 at 10:30
  • Oh I'm retarded. The "splice()" function returns not the affected array, but the array of removed elements. If you remove nothing, the result array is empty. – Hirvesh Feb 04 '17 at 10:34
  • For array manipulation, it's better to use `slice()` which returns a new array, it's the immutable style. – webdeb Feb 04 '17 at 10:42