React refs do not update between render

Charles Haro picture Charles Haro · Mar 31, 2015 · Viewed 15k times · Source

So I have this component

var LineItemRowsWrapper = React.createClass({
  current_lineitem_count: 0,

  getAjaxData: function(){
    var lineitem_data  = [];
    for(var i = 0; i < this.current_lineitem_count; i++){
        var data = this.refs['lineitem_'+i].getAjaxData();

        lineitem_data.push(data)
    }
    return lineitem_data;
  },

  getLineitems: function(){
    var self = this;

    var lineitem_components = [];
    this.current_lineitem_count = 0;
    if(this.props.shoot){
      var preview = this.props.preview;

      var lineitems = this.props.shoot.get_lineitems();


      lineitem_components = lineitems.map(function (item, index) {
          var ref_str = 'lineitem_'+self.current_lineitem_count;
          self.current_lineitem_count++;

          return (
            <LineItemRow item={item} key={index} ref={ref_str} preview={preview} onChange={self.props.onChange} />
          )
        });
      }

    return lineitem_components;
  },

  render: function() {
    var lineitems = this.getLineitems();
    return (
      <div>
        {lineitems}
      </div>
    )
  }
})

the first time lineitems are rendered the refs work like expected. But if I add a lineitem to this.props.shoot the refs object of this component does not change.

So for example say I had an array of 3 lineitems

 [i1,i2,i3]

this.refs would be

 {lineitem_0:{}, lineitem_1:{}, lineitem_2:{}}

and when I update my lineitem array to be

 [i1,i2,i3,i4]

this.refs does not change, it will still be

 {lineitem_0:{}, lineitem_1:{}, lineitem_2:{}}

why doesn't the refs object update between renders? The LineItemRow components update properly so I know its not something wrong on that front. Any insights would be much appreciated!

____Edit____ (requested to add more code for context)

var DocumentContent = React.createClass({
  contextTypes: {
    router: React.PropTypes.func.isRequired
  },

  getParams: function(){
    return this.context.router.getCurrentParams()
  },

  getInitialState: function() {
    return {
      shoot: ShootStore.get_shoot(this.getParams().shoot_id),
    }
  },

  componentWillMount: function() {  
    ShootStore.bind( 'change', this.onStoreUpdate );
  },

  componentWillUnmount: function() {  
    ShootStore.unbind( 'change', this.onStoreUpdate );
  },

  onStoreUpdate: function(){
    this.setState(this.getInitialState());
  },



  addLineItem: function() {
      ShootActions.create_lineitem(this.state.shoot.id);
  },


  update_shoot_timeout: null,

  update_shoot:function(){
    var self = this;
    window.clearTimeout(this.update_shoot_timeout)
    this.update_shoot_timeout = window.setTimeout(function(){

        var lineitem_data = self.refs.lineitems.getAjaxData()

        if(self.props.shoot){
            ShootActions.update_shoot(self.state.shoot.id, lineitem_data )
        }
    }, 500)
  },


  render: function() {

    var shoot = this.state.shoot;
    return (
        <div className='document__content'>
            <div className='row'>


            <div className='document__expenses'>
                <h3 className='lineitem__title'> Expenses </h3>
                <LineItemRowsWrapper shoot={shoot} onChange={this.update_shoot} ref='lineitems'/>

            </div>
            <button onClick={this.addLineItem} className="btn-small btn-positive">   
                       + Add Expense
                    </button>  




        </div>
    );
  } 
})

Answer

jeff_kile picture jeff_kile · Oct 12, 2015

Under the section "Caution" in the react documentation about refs https://facebook.github.io/react/docs/more-about-refs.html

"Never access refs inside of any component's render method - or while any component's render method is even running anywhere in the call stack."

Which is exactly what you're doing.

Instead you should store state about the component in this.state or properties of the component in this.props