Change:
Do you want to request a feature or report a bug? Bug What is the current behavior? If you remove the ref from a mounted HostComponent on an update, it calls the original ref with null. If you remove the ref from a mounted ClassComponent on an update, it does not call the original ref with null. Note that it doesn't call ref1 (passed to FooInner as ref) with null Note that it does call ref1 (passed to div as ref) with null If you modify the ref from either a HostComponent or a ClassComponent (change from to ), the original ref (ref1) is called with null and the new ref (ref2) is called with the instance. (take the sandboxes above and change the second to What is the expected behavior? Honestly not sure but it should be consistent. The reason this is different is because for HostComponents, as long as . For ClassComponents, as long as -- which means if is null, the behavior is different (hence this issue). We should probably pick one and make it consistent. Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React? Unsure - it for sure is an issue in latest/Fiber, but I haven't dug deep enough to see what happened in <16.

Discussion:
ReactDOM = require('react-dom'); ReactTestUtils = require('react-dom/test-utils'); <ins> class ClassComponent extends React.Component { render() { return null; } } </ins> TestComponent = class extends React.Component { render() { <del> return ( <div> {this.props.destroy ? null : ( <div ref="theInnerDiv">Lets try to destroy this.</div> )} </div> ); </del> <ins> if (this.props.destroy) { return <div />; } else if (this.props.removeRef) { return ( <div> <div /> <ClassComponent /> </div> ); } else { return ( <div> <div ref="theInnerDiv" /> <ClassComponent ref="theInnerClassComponent" /> </div> ); } </ins> } }; });

expect( Object.keys(testInstance.refs || {}).filter(key => testInstance.refs[key]) .length, <del> ).toEqual(1); </del> <ins> ).toEqual(2); </ins> ReactDOM.unmountComponentAtNode(container); expect( Object.keys(testInstance.refs || {}).filter(key => testInstance.refs[key])

expect( Object.keys(testInstance.refs || {}).filter(key => testInstance.refs[key]) .length, <del> ).toEqual(1); </del> <ins> ).toEqual(2); </ins> ReactDOM.render(<TestComponent destroy={true} />, container); expect( Object.keys(testInstance.refs || {}).filter(key => testInstance.refs[key])

).toEqual(0); }); <ins> it('should remove refs when removing the child ref attribute', () => { const container = document.createElement('div'); const testInstance = ReactDOM.render(<TestComponent />, container); expect(ReactTestUtils.isDOMComponent(testInstance.refs.theInnerDiv)).toBe( true, ); expect( Object.keys(testInstance.refs || {}).filter(key => testInstance.refs[key]) .length, ).toEqual(2); ReactDOM.render(<TestComponent removeRef={true} />, container); expect( Object.keys(testInstance.refs || {}).filter(key => testInstance.refs[key]) .length, ).toEqual(0); }); </ins> it('should not error when destroying child with ref asynchronously', () => { class Modal extends React.Component { componentDidMount() {

function markRef(current: Fiber | null, workInProgress: Fiber) { const ref = workInProgress.ref; <del> if (ref !== null && (!current || current.ref !== ref)) { </del> <ins> if ( (current === null && ref !== null) || (current !== null && current.ref !== ref) ) { </ins> // Schedule a Ref effect workInProgress.effectTag |= Ref; }