I’ve been dabbling with React.js again and trying to understand what isn’t clicking for me with this popular frontend framework. Consider this some messy out loud thinking about React’s useEffect
hook.
What is useEffect?
The useEffect
function is a React Hook. React Hooks only work with functional components. That is to say, React Hooks will NOT work with class based components. A React functional component is a javascript function that renders a component. Functional components are not javascript constructor functions even though they’re often named with TitleCase.
React Hooks are also functions, but will only “work” when called while React is rendering a functional component. If you try to call a React Hook outside of functional component it will not work — you’ll see an error something like this
Error: Invalid hook call. Hooks can only be called inside of the body of a function
component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix
this problem.
This is a runtime check, and can’t be (easily) worked around with compiler shenanigans.
As a React programmer, you use the useEffect
hook when you want to run some code after React has rendered your component. This will most often happen after some value changes in your React.js component.
Why is this named useEffect?
To some programmers, the useEffect
hook looks like a way to register listener/observer functions. The name useEffect
doesn’t seem to have much to do with listener/observer functions.
React’s core programmers like to call my action not related to the rendering of a React Component a “side effect”. This includes things like making an ajax request in order to fetch data, or directly updating the browser’s DOM after React’s rendered your component. The side effect terminology comes from functional programming. Whether it’s accurately used here or not will depend on your point of view.
The Three Forms of useEffect
There are three general ways to call useEffect
. One instructs useEffect
to call your function whenever a variable value changes. A second instructs useEffect
to call your function the first time a component loads. The final form tells useEffect
to call your function every time the component renders.
To use the first form, you’ll write code that looks like this
useEffect(function(){
// your code
},[variable1, variable2]);
The list of variables are the values that useEffect
will look for changes in. If those values have changed since the previous render, your code (// your code
above) will run. These variables are usually values that come from the state hook or new prop values, but there’s nothing stopping you from using regular variables here.
The second form, the form that allows you to run code the first time a component renders, looks like this
useEffect(function(){
// your code
},[]);
This is similar to the first form, except that you pass an empty array to useEffect
.
Finally, the final form looks like this
useEffect(function(){
// your code
});
In this form we’ve left of the second argument to useEffect
. This instructs useEffect
to always call our effect function after React renders our component.
Cleanup Functions
The useEffect
hook also allows you register a cleanup function. A cleanup function is a function that runs
- When a component unmounts
- Before every component render except the first
The intended use of a cleanup function is to run code that will “undo” anything code in your main effect function did. This might be clearing an interval timer, unsubscribing from a web-socket listener, etc.
To register a cleanup function you’ll use code that looks like this.
useEffect(function(){
// your code
function myCleanupFunction() {
}
return myCleanupFunction
});
That is, your effect function should return the function you want to use as a cleanup function. In the above example React will register the returned myCleanupFunction
function as a cleanup function.
Things I do not Like
First — the name useEffect
feels disconnected from how a working React programmer is going to use this feature. If I’m writing user interface code I’m not thinking about side effects and immutability. I’m thinking “The user did X in my UI. I need to make sure these other things happen”. The useEffect
names serves the programmers who created React over the programmers who will be using React.
Next, the simplest form of useEffect
is the most dangerous. By simplest form, I mean calling useEffect
with a single argument
useEffect(function(){
// your code
});
This “simplest” form (without a dependency array) means your code runs on every render. This has the greatest potential to cause performance problems in an application and is the form you need to take the most care with. A friendlier API would make the default behavior less potentially dangerous.
Third — the useEffect
APIs are non-obvious. As a React programmer I might think “I want to run this code when the component starts up”. To do that, I add an empty dependency array to the useEffect
call.
useEffect(function(){
// your code
},[]);
This is a less than obvious API. As someone who’s coming to React new, when I see that empty array it in no way conveys the idea that this code will only run on startup. The only folks who might draw that conclusion are folks who are already familiar with the implementation details of React, or folks who’ve learned it as an arbitrary rule. You can’t reason your way to that behavior without diving into the guts of React.
The API for creating a cleanup function
useEffect(function(){
// your code
return function () {
}
});
is similarly opaque. As a javascript programmer (either experienced or new) I look at the above code and think to myself “OK, my useEffect
function returns another function, but when does React call that function”. Again, without diving into React’s internals or learning arbitrary rules you can’t know.
My biases favor APIs that are easy to learn by being either explicit or offering hints about what they do. My experience of React’s APIs have been the exact opposite of this.
Put another way — React is designed to express its characteristics as a framework for “immutable programming” — it’s not optimized for reasoning about and implementing a user interface.
Why does React Succeed?
So, despite a design that’s unfriendly to its end users, why does React succeed?
First, its core concept — of using plain javascript objects to build up a light weight version of the DOM tree (sometimes called a shadow DOM) and only update the real DOM when you need to — is a great one. This both boosts performance/speed in browser based environments and allows React code to be more easily ported to other UI environments (Desktop programs, Mobile programs, etc).
More than that though, I think React owes its success to solid product management. When React first landed a lot of people rolled their eyes at Yet Another Javascript Framework that would be gone in a year. The infamous javascript hype cycle.
But React stuck around. The folks managing the product used the javascript hype cycle to their advantage. By releasing a steady stream of new features and APIs the framework that ended up replacing React was — React. Most javascript frameworks implement some core great idea, make some minor improvements to it over time, and then the authors/maintainers need to move on to day jobs. Another human has a better idea and releases a new framework and the cycle repeats. With React each generation of features has been compelling enough to keep early adopter type folks around and invested in the platform. Why go look at a new framework when React has a new hooks API to look at?
React’s complexity also creates an audience/market for tutorials — both in depth material developed by professionals and low quality content mill articles. This saturation of content ends up being free marketing for the framework.
One elephant in the room is that a lot of this success is tied heavily to the number of people that Facebook throws behind React. The engineers to keep features coming, the writers to give the community somewhere to start, and the product people working hard to make sure what’s put out is a cogent whole.
The uncomfortable truth is a well designed API doesn’t guarantee success and an average, or even confusing, API can be shored up if a big company puts enough resources behind it.