Why you shouldn't put refs in a dependency array
If you use a ref in your effect callback, shouldn't it be included in the dependencies? Why refs are a special exception to the rule!

Before we get started, here's a brief look of valid (today) ways to import React and use the useState
hook:
// globalwindow.React.useState()// CommonJSconst React = require('react')React.useState()// ESModules default importimport React from 'react'React.useState()// ESModules named importimport {useState} from 'react'useState()// ESModules namespace importimport * as React from 'react'React.useState()
Below I'll explain where each of these mechanisms came from and why I prefer the last one.
I started writing React code back in the React.createClass
days. Here's how we did things back then:
var React = require('react')var Counter = React.createClass({ propTypes: { initialCount: React.PropTypes.number.isRequired, step: React.PropTypes.number, }, getDefaultProps: function () { return {step: 1} }, getInitialState: function () { var initialCount = this.props.hasOwnProperty('initialCount') ? this.props.initialCount : 0 return {count: initialCount} }, changeCount: function (change) { this.setState(function (previousState) { return {count: previousState.count + change} }) } increment: function () { this.changeCount(this.props.step) }, decrement: function () { this.changeCount(-this.props.step) }, render: function () { return ( <div> <div>Current Count: {this.state.count}</div> <button onClick={this.decrement}>-</button> <button onClick={this.increment}>+</button> </div> ) },})
Yup, var
, React.createClass
, require
, function
. Good times.
Eventually, we got ES6 (and later) which included modules, classes, and some other syntax niceties:
import React from 'react'class Counter extends React.Component { state = {count: this.props.initialCount ?? 0} changeCount() { this.setState(({count}) => ({count + change})) } increment = () => this.changeCount(this.props.step) decrement = () => this.changeCount(-this.props.step) render() { return ( <div> <div>Current Count: {this.state.count}</div> <button onClick={this.decrement}>-</button> <button onClick={this.increment}>+</button> </div> ) }}
This is when the "how should I import React" question started popping up. A lot of people preferred doing this:
Normally this sort of thing isn't even a question. You just do what the library exposes. But React never actually exposed ESModules. It exposes itself as either a global variable called React
or a CommonJS module which is the React
object which has Component
on it (along with other things). But because of the way the code is compiled, both approaches technically work and neither is technically "wrong."
One other note... Looking at the code above you might wonder why we can't change that to:
- import React, {Component} from 'react'+ import {Component} from 'react'
The reason you need React
in there is because (at the time) JSX was compiled to use React:
- <button onClick={this.increment}>+</button>+ React.createElement('button', {onClick: this.increment}, '+')
So if you use JSX, you need to have React
imported as well.
It wasn't long after this that function components became a thing. Even though at the time these couldn't actually be used to manage state or side-effects, they became very popular. I (and many others) got very accustomed to refactoring from class to function and back again (many people just decided it was easier to use class components all the time).
For me, I preferred using function components as much as possible and this is likely the reason that I preferred using import React from 'react'
and React.Component
rather than import React, {Component} from 'react'
. I didn't like having to update my import statement any time I refactored from class components to function components and vice-versa. Oh, and I know that IDEs/editors like VSCode and WebStorm have automatic import features, but I never found those to work very well (I ran into stuff like this all the time).
Oh, and one other interesting fact. If you were using TypeScript instead of Babel, then you'd actually be required to do import * as React from 'react'
unless you enabled allowSyntheticDefaultImports
.
Then hooks took the scene and the way I wrote components evolved again:
import React from 'react'function Counter({initialCount = 0, step}) { const [count, setCount] = React.useState(initialCount) const decrement = () => setCount((c) => c - step) const increment = () => setCount((c) => c + step) return ( <div> <div>Current Count: {count}</div> <button onClick={decrement}>-</button> <button onClick={increment}>+</button> </div> )}
This brought up another question on how to import React. We had two ways to use hooks:
import React from 'react'// ...const [count, setCount] = React.useState(initialCount)// alternatively:import React, {useState} from 'react'// ...const [count, setCount] = useState(initialCount)
So again, should we do named imports, or just reference things directly on React
? Again, for me, I prefer to not have to update my imports every time I add/remove hooks from my files (and again, I don't trust IDE auto-import), so I prefer React.useState
over useState
.
Finally, React 17 was released with basically no real breaking changes but an announcement of great import (pun intended): There's a new JSX transform that means you no longer need to import React to transform JSX. So now you can write:
function App() { return <h1>Hello World</h1>}
And this will get compiled to:
// Inserted by a compiler (don't import it yourself!)import {jsx as _jsx} from 'react/jsx-runtime'function App() { return _jsx('h1', {children: 'Hello world'})}
So the import happens automatically now! That's great, but that also means that if you want to migrate to use this new capability, you'll need to remove any import React from 'react'
that's just for JSX. Luckily the React team made a script to do this automatically, but they had to make a decision on how to handle situations where you're using hooks. You have two options:
import * as React from 'react'const [count, setCount] = React.useState(initialCount)// orimport {useState} from 'react'const [count, setCount] = useState(initialCount)
Both of these work today, are technically correct, and will continue to work into the future when React finally exposes an official ESModules build.
The React team decided to go with the named imports approach. I disagree with this decision for reasons mentioned above (having to update my import all the time). So for me, I'm now using the import * as React from 'react'
which is a mouthful for my hands to type, so I have a snippet:
// snippets/javascript.json{ "import React": { "prefix": "ir", "body": ["import * as React from 'react'\n"] },}
For me, I'm sticking with import * as React from 'react'
so I don't have to worry about my imports. And that's my overly long blog post answer for a very common question I get. Hope it was helpful!
Delivered straight to your inbox.
If you use a ref in your effect callback, shouldn't it be included in the dependencies? Why refs are a special exception to the rule!
I still remember when I first heard about React. It was January 2014. I was listening to a podcast. Pete Hunt and Jordan Walke were on talking about this framework they created at Facebook that with no support for two way data-binding, no built-in HTTP library, no dependency injection, and in place of templates it had this weird XML-like syntax for the UI. And to top it all off, I was listening to it while driving to the first ever ng-conf.
Stop UI tearing in React! useSyncExternalStore safely syncs external data. Get best practices for this advanced hook (like stable functions) at EpicReact.dev.
Simplify and speed up your app development using React composition