Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refs callback is called twice on every render, even when component instance is reused #8619

Closed
natevw opened this issue Dec 22, 2016 · 7 comments

Comments

@natevw
Copy link

natevw commented Dec 22, 2016

Do you want to request a feature or report a bug?

Bug, perhaps just in documentation.

What is the current behavior?

The documentation for Refs and the DOM says:

The ref attribute takes a callback function, and the callback will be executed immediately after the component is mounted or unmounted.

But if I create a stateful component like:

class ScrollHelper extends React.PureComponent {
  
  componentDidMount() {
    console.log("ScrollHelper componentDidMount")
  }

  componentWillUnmount() {
    console.log("ScrollHelper componentWillUnmount")
  }
  
  render() {
    console.log("ScrollHelper render")
    return <div/>
}

and use it via <ScrollHelper ref={(obj) => console.log("ScrollHelper ref", obj)}/> within an app, I do not see the behavior documented, but rather the following pattern of logs:

// [first render]
ScrollHelper render
ScrollHelper componentDidMount
ScrollHelper ref <{props: etc…}>
// [some data changes elsewhere in the app]
ScrollHelper ref <null>
ScrollHelper render
ScrollHelper ref <{props: etc…}>
// [some data changes elsewhere in the app, again…]
ScrollHelper ref <null>
ScrollHelper render
ScrollHelper ref <{props: etc…}>
// [some data changes causing the ScrollHelper to actually unmount]
ScrollHelper ref <null>
ScrollHelper componentWillUnmount

What is the expected behavior?

According to the docs, I would expect my refs callback to be called exactly two times during the events above — once (with the component) after the "ScrollHelper componentDidMount" and once (with null) before the "ScrollHelper componentWillUnmount". Instead it gets "reset" basically every time the page is re-rendered — even though it's pretty clearly ending up with the same component instance each time! (I verified this is true via some additional code as well, but should be evident from the lack of additional "componentDidMount" logs anyway.)

Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?

react@15.4.1, no idea what the behavior was earlier.

@natevw
Copy link
Author

natevw commented Dec 22, 2016

I think I figured this out, using a clue in the code comments of

// If either the owner or a `ref` has changed, make sure the newest owner
:

If either [sic] the owner or a ref has changed, make sure the newest owner has stored a reference to this, and the previous owner (if different) has forgotten the reference to this.

If I change my usage to

// global for purpose of illustration
function LogRef(obj) {
  console.log("LogRef called", obj)
}


// within app's render code somewhere…
    <ScrollHelper ref={LogRef}/>

…I then get the expected result!

What was happening above is, during every render, a new anonymous function is instantiated for the (obj) => console.log(obj) code. The ReactRef logic treats this as a new watcher and feels obligated to notify both the old and new callback instances about the "handoff".

So the workaround is to pass some sort of stable callback, like a pre-bound method or a long-lived function instance.

I will leave this open, because this "handoff" behavior is not documented at https://facebook.github.io/react/docs/refs-and-the-dom.html, nor is this implication when using inline functions highlighted (as a potential pitfall).

@hyy1115
Copy link

hyy1115 commented Dec 25, 2016

I have a question, can help me?

There are two sub-components, radio and select, in the parent container through the switch call the two components, and then need to traverse the component in the parent container call to get the number of components within the method.

I try to write in the following way, is wrong.
E.g

container:

renderComs() {
          let coms = components.map(component => {
                 switch(component.type) {
                     case 'type-1':
                         return (
                             <SelectCom ref="select" />
                          )
                     case 'type-2':
                          return (
                              <RadioCom ref="radio" />
                         )
                      default:
                          return false
                  }
           }) 
           this.coms = coms
           return coms
     }

   clickFunction() {
       let com = this.coms
       for (let i = 0; i < com.length; i++) {
            console.log(this.refs.com[i].getValue()) //Get the getValue method inside the subcomponent via refs,but (this.refs.com[i]) is error
       }
   }

Does not seem to work this way through this.refs.com [i]

@davidmccabe
Copy link
Contributor

Also ran into this. It's particularly a problem when a parent component maintains an array of refs to its children. One might expect this code to work properly:

render(): React.Element<any> {
  this._childRefs = [];
  return (
    {React.Children.map(this.props.children, (child, i) =>
      <ChildContainer ref={r => this._childRefs[i] = r}>
        {child}
      </ChildContainer>,
    )}
  );
}

However, if a child goes away, so that there are only n-1 children, then the ref-setting function is called n times with argument null, resulting in _childRefs being too long and having an unexpected null. In this case, the notion of having the old owner forget about the ref is ironically reversed.

@hyy1115
Copy link

hyy1115 commented Dec 29, 2016

Has been resolved. Thank you.
this.refs[com[i].ref].getValue()

Perhaps this.refs on behalf of the key

@ajbeaven
Copy link

ajbeaven commented Jul 3, 2017

You might want to take a look at #9328 which explains why React behaves like this.

@gaearon
Copy link
Collaborator

gaearon commented Jul 3, 2017

Closing as a duplicate of #9328 (it is the other way around, but that issue got more attention).

@gaearon gaearon closed this as completed Jul 3, 2017
@natevw
Copy link
Author

natevw commented Jul 6, 2017

@gaearon Works for me, looks like this behavior did get documented as a caveat since I filed this; that the main reason I had left this one open anyway. Thanks!

For those finding this issue, an in-depth explanation of the (admittedly surprising at first) logic is here: #9328 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants