React - adding and editing new line items, how to manage state?

Here is a screen capture of a new resource form. To make the form user friendly, Whenever I add a new question or a new option for a question, I want to make that new item automatically open in an editing mode. Currently I have 4 nested components for this…

Questions (handles all the questions)
-QuestionItem (renders each question)
–Options (Handles all the options for each question)
—OptionItem (renders each option for each question)

All of these sit inside of the parent component where state is held (named Resource).

So far I have been able to add an entire new resource with new questions, new options, etc, but when I add a new question I have to then click the edit button…or when I add a new option, I click the edit icon next to the option. These localized renders for edit or display are handled at the component state level, not the top level.

This makes the new entry form very clunky to use and not intuitive for a new user. Add the question, edit the question, add the option, edit the option, then save. add a 2nd option, edit the 2nd option, then save, etc.

I am able to add a field called “edit” that is part of state and can live inside of each option or question when I add a new one, and then check for this state when the Question Item or Option Item is rendered.

However this really complicates the code, because then I have these extra fields laying around inside my top level state objects ( my actual data) with every queston and every option having it’s own “edit” field, that I’ll have to programmatically set to true or false.

Doing this locally in state is much easier but it’s less fine tuned…ie adding a new option or new question must occur at the top level since that’s where the data is contained in state. But the editing render of a new optoin occurs at the component level. I can’t set the new state of editing to be false inside the component level, I’d hvae to create another fucntion at the top level and pass it through props to receive the option item, and at the top level, set the editing field to false, then pass it back down through state.

I only need this edit field at creation, again for usability…add a new optoin and it’s already in edit mode.

Are there better ways to manage this? Here is a screen shot so you can see what I"m doing, and i’ll share some code snippets below:

Here’s the optionItem code. editing state is stored locally, so the user cal click on the edit icon for any option at any time and update it. This is a different action than what I’m describing above where “edit”: true would be added to the option array when a new optoin is added so it can render the first time in edit mode.

class OptionItem extends Component {
  constructor (props) {
    super(props)
    this.state = {
      editingOption: false
    }
    this.editOption = this.editOption.bind(this)
    this.saveOption = this.saveOption.bind(this)
    // this.handleChange = this.handleChange.bind(this)
  }

  render() {
    if (this.props.isEditing || this.state.editingOption || this.props.option.edit) {
      return this.renderEdit()
    } else {
      return this.renderDisplay()
    }
  }

  editOption(){
    console.log("edit icon pressed for option" + this.props.optionNum )
    this.setState({
      editingOption: true
    })
  }

  // handleChange(){
  //   console.log("Option edited")
  //   this.props.handleOption(this.props.questionNum, this.props.optionNum)
  // }

  saveOption(){
    console.log("save icon pressed for option: " )
    this.setState({
      editingOption: false
    })
  }

This code snippet is at the top level component where the new option or new question is added…I’ve added a field, “edit”: true to pass down to the component, which can be checked as in the OptionItem.js snippet I included here.

  addNewQuestion()  {
    const newQuestion =  {"title": "",
      "description": "",
      "options": [
      {
        "value": "",
        "description": ""
      }],
      "edit": true,
    }
    console.log([...this.state.questions, newQuestion])
    this.setState((prevState) => {
      return {questions: [...prevState.questions, newQuestion]};
    });
  }

I am fairly sure I can programatically sort this out with the structure I’ve used so far, (ie turn the edit field to false for the option after it is saved) but since this is at the top level and not the local state level (where I also have an isEditing field), it just becomes more complex and prone to confusion/mistakes.

Any suggestions?

I’m not sure if I’m following your explanation correctly, but maybe

  • Give each option group it’s own group-id, and share that id with it’s children.
  • Pass down a state with the id of the newly created group
    • if using react 16, use the context api to make it easier
  • I believe focus events bubble, and you’re using input fields, so
    • send a function down to reset the current id to null when onBlur fires for any of the fields.

This is really something that redux would be better suited for, since you wouldn’t have to do the ‘pass state and callback dance’. It would just take two functions, one to dispatch a setter, one to reset it.

Thanks, i am using 16, and I have glanced at context API, but don’t quite comprehend it yet.

I was a little worried someone would mention Redux for this…i’m sure it would be a better implementation, but I was feeling impatient and just wanted to get it working.

I’ve check out several redux tutorials (including Dan Abrams’) and it’s still not sticking. Do you have a good suggestion for learning it?

For redux I used Dan’s videos, and Stephen Grider’s react/redux tutorials on Udemy. But if you don’t want to use redux, let me give a try at explaining the context api first.

The context api passes a value to a child component as long as the child has a consumer component listening, no matter how deep in the tree it is.

// First you assign a variable to an instance of the context you want, 
// with a default value as an argument, and it will return a react component
const myStringContext = React.createContext('I am just a string')

// The default can be whatever you want
const myThemeContext = React.createContext({ theme: 'light' })

// You can then use it like a component to wrap your own
<myThemeContext />
<myStringContext />

— In the Parent Component —

class Parent extends React.Component {
...
  state = {
   myTheme: {
     background: 'dark' 
   }
  }
...
  render() {
    // Use the Provider method on the component instance  
    // to pass a value to the tree below, using the value prop.

    return (
      // Here we're passing "{ background: 'light' }" as the new value.
      <myThemeContext.Provider value={ this.state.myTheme }>
        <Child1 />
      </myThemeContext.Provider>
    
      // Here we're not passing a value, so we'll get the default value 
      <myStringContext.Provider >
        <Child2 />
      </myStringContext.Provider >
    );
  }
}

— In the Child Components —

  • Use a Consumer to read the current context.
  • You must use the same component instance, so make sure to export it if in a different file
  • the consumer will return the component’s context in a render prop, and you return a component tree from it
function Child2(props) {

  // In this example, the current value is the default.
  return (
    <myStringContext.Consumer >
     // render prop function. It's just a regular function
      {string => <Button {...props} />} 
    </myStringContext.Consumer>
  );
}

function Child1(props) {

 // In this example, the current value returned for the theme is "dark".
  return (
    <myThemeContext.Consumer>
      // Here we receive the context and set it to the theme prop
      {theme => <Button {...props} theme={theme} />}
    </myThemeContext.Consumer>
  );
}

The provider component sets the value prop, and anywhere you use it’s consumer, it will have access to that value

If there’s something I should make clearer, let me know we’ll figure it out.

1 Like

Thanks for this write up! I went back and reread the docs, and after reading your explanation and my use case, this makes a lot more sense. It may be verbose to set up producers and consumers, but seems like it could save the callback hell I’ve been dealing with.

I think I’ll refactor with this first, then I can buy time to learn React. Thanks again for your help.

1 Like