Something that’s really important to know about React’s useEffect
hook is that it eagerly attempts to synchronize the “state of the world” with the state of your application. That means that your effect callback will run every time your component is rendered. This normally won’t lead to bugs (in fact, it does a great job at preventing bugs that plagued React apps before useEffect
was available), but it can definitely be sub-optimal (and in some cases can result in an infinite loop).
In this lesson we’ll observe that our effect callback is getting called more than it needs to be, and you’ll learn how to add a dependency array so it is updated only when the state it relies on changes. And in real applications, you’ll want to make sure you have and follow the rules from the ESLint plugin: eslint-plugin-react-hooks (many tools like Create React App have this installed and configured by default).
Kent C. Dodds: [0:00] We've added a little bit here to simulate another optimization that we want to make. Here now we have a count state, and that's just being rendered into this button. Every time we click on the button, it re-renders our app component, which triggers a re-render of our greeting component.
[0:16] Every time our greeting component re-renders, this useEffect is going to be called. If we console.log('greeting useEffect'), we'll save that. Open up our DevTools. We see we get the greeting useEffect for the initial render. Every time I click this, we'll get another greeting useEffect.
[0:35] The effect that we're running is updating localStorage to set the name to the name value. This side effect doesn't need to be run because the name value hasn't changed.
[0:46] We only need to synchronize the localStorage value with the name value that we have in memory for the state of this component.
[0:54] React useEffect accepts a second argument as an optimization to combat this problem. That second argument is a dependency array where you pass all the dependencies for your side effect. This is where you pass anything that you want to make sure you synchronize the state of the world with the state of our application.
[1:13] In our case, the only thing that we're trying to synchronize here is the state of localStorage, which is the state of the world, with the state of our application, which is the name. For our dependency array, we're going to provide the name.
[1:27] If we save that, then we'll notice that we get that greeting useEffect called initially, and that's because useEffect is always going to be called on the initial mount. Hereafter, when we click on this button, we're not going to get the greeting useEffect.
[1:44] Then when we update the name value, we're going to get greeting useEffect called because the name has changed, and the state of the world is now out of sync with the state of our application. React is going to call our useEffect callback.
[1:58] It's very important that you keep this dependency array accurate according to the dependencies that your callback function relies on. For example, we rely on name. That's why it's important that we keep this dependency list in here.
[2:11] If you don't keep this list accurate, then you could be missing out on synchronizing the state of the world with state changes that happen in your app, and your users could lose saved work.
[2:21] For example, if I were to remove the name from this array and save that, then we'll get that greeting useEffect called. As I type in here, we'll notice that the state of my application is being kept updated, but I'm not getting any console.logs.
[2:37] If I refresh, I'm going to be back to where I was before. I, as the user of this application, have lost work, because my code was not properly synchronizing the state of my application with the state of the world outside my application.
[2:51] In an effort to help you avoid making mistakes here, the React team has created an ESLint plugin called eslint-plugin-react-hooks which you can use to not only ensure that the dependency array is kept up-to-date, but keep it up-to-date automatically for you using ESLint's fix feature.
[3:09] The rule that will help you with this is the exhaustive-deps rule, and I strongly advise that you use this tool and follow that rule.
[3:17] In review, the problem that we were solving here is our effect callback was being called more than it needed to be. I want to make a special note here, that just because it was being called more than it needed to be didn't mean we had a bug in our application, so this is just an optimization to make our application run a little faster.
[3:35] A dependency array may not be necessary in all cases, but in our case, we could simply add this dependency array with the one dependency that our effect callback relied on. This callback is only called when necessary.
This can be a handy tool to avoid runaway hooks :) https://github.com/kentcdodds/stop-runaway-react-effects
thx @kent - looks like a useful / time saving alternative to force quitting the browser.
Are there any performance issues with trying to compartmentalize each element with its own useEffect call and dependency?
"...in some cases can result in an infinite loop" - yes true. In fact since I've switched to using mainly hooks - I've learned first hand that useEffect hooks are great for accidentally creating run away code!