15

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>
    );
  } 
})
rene
  • 41,474
  • 78
  • 114
  • 152
Charles Haro
  • 1,866
  • 3
  • 22
  • 36
  • 1
    How and when do you update the lineitem list? What kind of function is passed into this.pros.onChange? Do you have more relevant code? – magnudae Mar 31 '15 at 08:39
  • 1
    I'm using flux and so the lineitem list is updated from a data_store and being passed in by the parent component. I have already checked and this is all working correctly. The onChange function being passed in saves the data to the backend and updates the store when a lineitem is change. The onchange function calls the getAjaxData(). method This is the most relevant part of the code, but i'll post my other code. Everything else is working as it should. I'm pretty sure this is a react thing. Because the {lineitems} component list that is being updated is correct and even has the correct refs. – Charles Haro Mar 31 '15 at 08:42
  • 1
    I read some more about refs on facebooks documentation. I became a little uncertain if it is possible to dynamically add more refs after initial render. I can not see any faults in your code so this is my only theory atm. Keep searching, maybe you can find something [here](https://facebook.github.io/react/docs/more-about-refs.html). (if you haven't already tried) – magnudae Mar 31 '15 at 08:53
  • 1
    Yea that's what I thought too. Seems weird that they wouldn't update refs after each render though. I feel like I can't be the only one trying to mess with code this way. Maybe I'm doing something inherently wrong, or frowned upon by the react creators? I found this http://www.mattzabriskie.com/blog/react-referencing-dynamic-children but it doesn't seem to be what I need since I'm not using children. – Charles Haro Mar 31 '15 at 08:56
  • 2
    From my impression of what the documentation says about refs, is that it should be avoided. I see you are using refs for the LineItemRowsWrapper. I think you could have passed the ajaxData from the LineItemsRowsWrapper to the onChange sent from from the DocumentContent without using ref. Then you could relieve yourself of that ref. A ref is mostly for accessing data in the html after render. You can use it to access children, but there are other maybe more "Reactier" ways of doing that. It does not solve your problem though (I think). – magnudae Mar 31 '15 at 09:06
  • Sigh I thought as much, I can hack a solution. I just thought there is a better way to do this. I'm guessing the "React" way would to make it so that each component updates the store individually huh. I'm cheating by making it doing all of the lineitems anytime one of them changes – Charles Haro Mar 31 '15 at 09:13
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/74179/discussion-between-charles-haro-and-magnudae). – Charles Haro Mar 31 '15 at 09:23
  • Could you just store the list of rows in an array in your wrapper and use that to call methods rather than artificially creating `ref` strings? – WiredPrairie Mar 31 '15 at 10:56
  • I'm pretty sure that elements i'm passing to render are not the actual elements that get rendered. https://facebook.github.io/react/docs/more-about-refs.html – Charles Haro Mar 31 '15 at 10:58
  • Question: why are you using refs, do you need to access them later on (though this would not be a proper react.js flow)? Also I'd recommend storing current_lineitem_count into the state of the component rather than into the component itself. – Cristik Apr 12 '15 at 10:06

2 Answers2

14

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

jeff_kile
  • 1,805
  • 16
  • 21
0

Remove all your refs and iteration to read the data.

The onchange handler you pass all the way down to the LineItem component should be called and passed only the data that changes. (The single LineItem data)

This is then handled back at the component with the state handling (DocumentContent).

Create an action ShootActions.updateLineItem() that updates the relevant line item in the store, which then emits the change and everything renders again.

dannyshaw
  • 368
  • 2
  • 6