Peter Breen
Peter Breen

Reputation: 447

Trigger Re-Render of Child component

I'm new to React and am running into the same problem a few times. In this particular situation, I'm trying to get an option in a select dropdown to update when I update a text input.

I have a parent, App, with the state attribute "directions", which is an array. This gets passed as a property to a child, GridSelector, which creates the text field and dropdown. When the text field is changed, a function triggers to update the parent state. This in turn causes the GridSelector property to update. However, the dropdown values, which are originally generated from that GridSelector property, do not re-render to reflect the new property value.

I'm trying to figure out the most React-ful way to do this and similar manuevers. In the past, I've set a state in the child component, but I think I've also read that is not proper.

My working site is at amaxalaus.bigriverwebdesign.com

Here's the pertinent code from each file:

App.js

class App extends React.Component {
    constructor(props){
      super(props);
      this.state = {
       directions: [],
       dataRouteDirections: '/wp-json/wp/v2/directions',
       currentDirectionsIndex: 0
     }
     this.addImageToGrid = this.addImageToGrid.bind(this);
     this.changeTitle=this.changeTitle.bind(this);
   }


 componentDidMount(){
    fetch(this.state.dataRouteDirections)
        .then(data => data=data.json())
        .then(data => this.setState({directions:data}));

  }

  addImageToGrid(image) {
    this.refs.grid.onAddItem(image);  //passes image add trigger from parent to child
  }

  createNewDirections(){
    var directions= this.state.directions;
    var index = directions.length;
    var lastDirections = directions[directions.length-1];
    var emptyDirections= {"id":0,"acf":{}};
    emptyDirections.acf.grid="[]";
    emptyDirections.acf.layout="[]";
    emptyDirections.title={};
    emptyDirections.title.rendered="New Directions";

    if (lastDirections.id!==0 ) { ///checks if last entry is already blank
      this.setState({
        directions: directions.concat(emptyDirections),  //adds empty directions to end and updates currentdirections
        currentDirectionsIndex: index
      });
    }
  }

  changeTitle(newTitle){
    var currentDirections = this.state.directions[this.state.currentDirectionsIndex];
    currentDirections.title.rendered = newTitle;
  }

  render() {
    var has_loaded;  //has_loaded was added to prevent double rendering during loading of data from WP
    this.state.directions.length > 0 ? has_loaded = 1 : has_loaded = 0;

    if (has_loaded ) {
     /* const currentGrid = this.state.directions;*/
    return (  //dummy frame helpful for preventing redirect on form submit

        <div>

          <div className="fullWidth alignCenter container">
            <GridSelector 
              directions={this.state.directions} 
              currentDirectionsIndex={this.state.currentDirectionsIndex} 
              changeTitle={this.changeTitle}
            />
          </div>
          <Grid ref="grid" 
            currentGrid={this.state.directions[this.state.currentDirectionsIndex]}
          />
          <ImageAdd addImageToGrid={this.addImageToGrid}/>
          <div className="fullWidth alignCenter container">
            <button onClick={this.createNewDirections.bind(this)}> Create New Directions </button>
          </div>

        </div>

      )
    } else {
      return(
        <div></div>
      )
    }
  }
}

GridSelector.js

class GridSelector extends React.Component {    

  constructor(props) {
    super(props);
    var currentDirections = this.props.directions[this.props.currentDirectionsIndex];
    this.state = {
        currentTitle:currentDirections.title.rendered
    }
  }
    createOption(direction) {
        if (direction.title) {
            return(
                <option key={direction.id}>{direction.title.rendered}</option>
            )
        }   else {
            return(
                <option></option>
            )
        }
    }    

    handleChangeEvent(val) {
        this.props.changeTitle(val);  //triggers parent to update state
    }    

    render() {

        return(
            <div>
                <select name='directions_select'>
                    {this.props.directions.map(direction => this.createOption(direction))}              
                </select>    

                <div className="fullWidth" >
                    <input 
                        onChange={(e)=>this.handleChangeEvent(e.target.value)}
                        placeholder={this.state.currentTitle}
                        id="directionsTitle"
                    />
                </div>
            </div>
        )
    }
}

Upvotes: 1

Views: 283

Answers (1)

Dennie de Lange
Dennie de Lange

Reputation: 2934

You made a very common beginner mistake. In React state should be handled as an immutable object. You're changing the state directly, so there's no way for React to know what has changed. You should use this.setState.

Change:

  changeTitle(newTitle){
    var currentDirections = this.state.directions[this.state.currentDirectionsIndex];
    currentDirections.title.rendered = newTitle;
  }

To something like:

  changeTitle(newTitle){
    this.setState(({directions,currentDirectionsIndex}) => ({
         directions: directions.map((direction,index)=> 
         index===currentDirectionsIndex? ({...direction,title:{rendered:newTitle}}):direction
    })

Upvotes: 3

Related Questions