componentDidMount called BEFORE ref callback

Problem

I'm setting a react ref using an inline function definition

render = () => {
    return (
        <div className="drawer" ref={drawer => this.drawerRef = drawer}>

then in componentDidMount the DOM reference is not set

componentDidMount = () => {
    // this.drawerRef is not defined

My understanding is the ref callback should be run during mount, however adding console.log statements reveals componentDidMount is called before the ref callback function.

Other code samples I've looked at for example this discussion on github indicate the same assumption, componentDidMount should be called after any ref callbacks defined in render, it's even stated in the conversation

So componentDidMount is fired off after all the ref callbacks have been executed?

Yes.

I'm using react 15.4.1

Something else I've tried

To verify the ref function was being called, I tried defining it on the class as such

setDrawerRef = (drawer) => {
  this.drawerRef = drawer;
}

then in render

<div className="drawer" ref={this.setDrawerRef}>

Console logging in this case reveals the callback is indeed being called after componentDidMount


Solution 1:

Short answer:

React guarantees that refs are set before componentDidMount or componentDidUpdate hooks. But only for children that actually got rendered.

componentDidMount() {
  // can use any refs here
}

componentDidUpdate() {
  // can use any refs here
}

render() {
  // as long as those refs were rendered!
  return <div ref={/* ... */} />;
}

Note this doesn’t mean “React always sets all refs before these hooks run”.
Let’s look at some examples where the refs don’t get set.


Refs don’t get set for elements that weren’t rendered

React will only call ref callbacks for elements that you actually returned from render.

This means that if your code looks like

render() {
  if (this.state.isLoading) {
    return <h1>Loading</h1>;
  }

  return <div ref={this._setRef} />;
}

and initially this.state.isLoading is true, you should not expect this._setRef to be called before componentDidMount.

This should make sense: if your first render returned <h1>Loading</h1>, there's no possible way for React to know that under some other condition it returns something else that needs a ref to be attached. There is also nothing to set the ref to: the <div> element was not created because the render() method said it shouldn’t be rendered.

So with this example, only componentDidMount will fire. However, when this.state.loading changes to false, you will see this._setRef attached first, and then componentDidUpdate will fire.


Watch out for other components

Note that if you pass children with refs down to other components there is a chance they’re doing something that prevents rendering (and causes the issue).

For example, this:

<MyPanel>
  <div ref={this.setRef} />
</MyPanel>

wouldn't work if MyPanel did not include props.children in its output:

function MyPanel(props) {
  // ignore props.children
  return <h1>Oops, no refs for you today!</h1>;
}

Again, it’s not a bug: there would be nothing for React to set the ref to because the DOM element was not created.


Refs don’t get set before lifecycles if they’re passed to a nested ReactDOM.render()

Similar to the previous section, if you pass a child with a ref to another component, it’s possible that this component may do something that prevents attaching the ref in time.

For example, maybe it’s not returning the child from render(), and instead is calling ReactDOM.render() in a lifecycle hook. You can find an example of this here. In that example, we render:

<MyModal>
  <div ref={this.setRef} />
</MyModal>

But MyModal performs a ReactDOM.render() call in its componentDidUpdate lifecycle method:

componentDidUpdate() {
  ReactDOM.render(this.props.children, this.targetEl);
}

render() {
  return null;
}

Since React 16, such top-level render calls during a lifecycle will be delayed until lifecycles have run for the whole tree. This would explain why you’re not seeing the refs attached in time.

The solution to this problem is to use portals instead of nested ReactDOM.render calls:

render() {
  return ReactDOM.createPortal(this.props.children, this.targetEl);
}

This way our <div> with a ref is actually included in the render output.

So if you encounter this issue, you need to verify there’s nothing between your component and the ref that might delay rendering children.

Don't use setState to store refs

Make sure you are not using setState to store the ref in ref callback, as it's asynchronous and before it's "finished", componentDidMount will be executed first.


Still an Issue?

If none of the tips above help, file an issue in React and we will take a look.

Solution 2:

A different observation of the problem.

I've realised that the issue only occurred while in development mode. After more investigation, I found that disabling react-hot-loader in my Webpack config prevents this problem.

I am using

  • "react-hot-loader": "3.1.3"
  • "webpack": "4.10.2",

And it's an electron app.

My partial Webpack development config

const webpack = require('webpack')
const merge = require('webpack-merge')
const baseConfig = require('./webpack.config.base')

module.exports = merge(baseConfig, {

  entry: [
    // REMOVED THIS -> 'react-hot-loader/patch',
    `webpack-hot-middleware/client?path=http://localhost:${port}/__webpack_hmr`,
    '@babel/polyfill',
    './app/index'
  ],
  ...
})

It became suspicious when I saw that using inline function in render () was working, but using a bound method was crashing.

Works in any case

class MyComponent {
  render () {
    return (
      <input ref={(el) => {this.inputField = el}}/>
    )
  }
}

Crash with react-hot-loader (ref is undefined in componentDidMount)

class MyComponent {
  constructor (props) {
    super(props)
    this.inputRef = this.inputRef.bind(this)
  }

  inputRef (input) {
    this.inputField = input
  }

  render () {
    return (
      <input ref={this.inputRef}/>
    )
  }
}

To be honest, hot reload has often been problematic to get "right". With dev tools updating fast, every project has a different config. Maybe my particular config could be fixed. I'll let you know here if that's the case.