Categories


Archives


Recent Posts


Categories


A Critique of React’s useEffect Hook

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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

  1. When a component unmounts
  2. 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.

Copyright © Alan Storm 1975 – 2021 All Rights Reserved

Originally Posted: 19th April 2021