Modern React is broken

Suppose that you're a full stack developer. You get asked to join a project to develop some additional functionality but upon launching the lastest version, you discover that the front end application runs into a situation where it gets stuck in an infinite loop upon the page being opened and thus renders all of it inoperable. Of course, the components within the project are also likely to be strongly coupled, so it's probably not like you can just comment out an entire section of it, without breaking how the data fetching works. So, you'll have no choice but to embrace the fact that something is broken and, given that those changes are developed by someone else at a point in time where you don't know what exactly has changed and why, reverting stuff in the master branch would be a questionable choice. Especially because their changes could impact many of the components within the application (such as changing the header for most of the tables within the app) and reverting those could create a large amount of work down the line, once those need to be reintroduced - thus, needing a fix will become necessary, even though you might not understand their code either.

But you don't develop the code in a vacuum, where you have infinite amounts of time to get things done, either. In a somewhat dramatic interpretation of the events, you're attempting to get things out the door as quickly as you can, because you have 3 other projects to work on in your day job, need to work on your personal projects so you can't afford to work 11 hours per day on your work stuff, as well as have business breathing down on your neck and wondering about when the changes will be done. And yet, a seemingly related piece of functionality is now blocking your progress, while at the same time there are many other sub-optimal decisions that have been made in the project - creating custom components instead of using component libraries, even if the designers just keep linking you components that have already been made as a part of Semantic UI, as well as writing everything in TypeScript, which makes your development velocity hit the floor, because of the awkward integration with React. Not only that, but there are problems with the CI server because it's configured weirdly so that tests won't run, tests take forever to run locally and are hard to develop, as well as any other number of problems that you might run into.

Does that sound a dysfunctional development environment? Perhaps, but that's also the objective reality in many projects - unless you're developing it alone or discuss every architectural decision in detail beforehand and do proof of concept first, then you'll run into numerous things like that, especially if you're limited in your resources, short staffed, or both. So what does this have to do with React? Simple - it is one of those "death by a thousand cuts" situations. Normally, many of the problems above could be either fixed (like the CI issues) or coped with (like custom components everywhere), yet sooner or later something will come along that will make you suffer and even worse, block your progress, as the situation above did. To that end, i'd like to suggest that modern React and notably hooks are far more likely to lead to such problems than the old class based components did.

Approaches to components in React

But first, a bit of background. To be able to reason about the different types of components and their advantages as well as drawbacks, we should first see more about information about them.

Class based components

React originally only had class based components, which meant that every component within your app would have a corresponding JS class, typically as its own file. That also provided you with all of the methods that you'd get whilst extending the React.Component class, such as constructor, componentDidMount(), render etc., which allowed you to keep all of your code for a particular bit of the lifecycle in its own method. For example:

export default class ClassBasedComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            stateValue: 1
        };
    }
    render() {
        return <div>
            External prop: {this.props.propValue}<br/>
            Internal state: {this.state.stateValue}<br/>
            <button onClick={() => this.incrementState()}>Increment</button><br/>
            Is over: {this.isOver() ? "Yes" : "No"}

        </div>
    }
    incrementState() {
        const newStateValue = this.state.stateValue + 1;
        this.setState({
            stateValue: newStateValue
        });
    }
    isOver() {
        return this.state.stateValue > this.props.propValue;
    }
}

That results in a pretty simple component, which allows us to interact with it:

class-based-component-example

Purely functional components

Another approach that was added later, were functional components. Initially, there were purely functional ones, which offered a shorter syntax in comparison to the likes of Vue and Angular, allowing an entire component to be expressed as a single function, which would return the JSX that should be rendered. This allowed for simple display-only components to be created pretty quickly and seemed to work nicely. In practice, this is achieved in a pretty simple manner.

For example, if we'd extract the display part of the class based component above, we could get something like the following:

export default class ClassBasedComponentOkay extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            stateValue: 1
        };
    }
    render() {
        return <div>
            External prop: {this.props.propValue}<br/>
            Internal state: {this.state.stateValue}<br/>
            <button onClick={() => this.incrementState()}>Increment</button><br/>
            <IsOverDisplayComponent propValue={this.props.propValue} stateValue={this.state.stateValue}/>
        </div>
    }
    incrementState() {
        const newStateValue = this.state.stateValue + 1;
        this.setState({
            stateValue: newStateValue
        });
    }
}

function IsOverDisplayComponent(props) {
    const isOver = props.stateValue > props.propValue;
    return <span>
        Is over: {isOver ? "Yes" : "No"}
    </span>
}

Pretty good, right? Especially when you want the behaviour and data to be in one place, but the logic in another! Personally, i rather enjoy this approach.

Functional components with hooks

Eventually, the developers must have looked at the ability to create components without bothering with fully fledged classes and the lifecycle methods and decided to take the best of both worlds. In their approach, it looked like adding hooks to React - a way to store persistent state within functional components, by also indicating what their dependencies are and when they should be renewed. Theorethically, this should work pretty well and is seemingly similarly easy to implement.

For example, we could rewrite the class based component above as the following:

export default function FunctionalComponent(props) {
    const [stateValue, setStateValue] = useState(1);
    const incrementState = () => {
        const newStateValue = stateValue + 1;
        setStateValue(newStateValue);
    }
    return <div>
        External prop: {props.propValue}<br/>
        Internal state: {stateValue}<br/>
        <button onClick={() => incrementState()}>Increment</button><br/>
        <IsOverDisplayComponent propValue={props.propValue} stateValue={stateValue}/>
    </div>;
}

function IsOverDisplayComponent(props) {
    const isOver = props.stateValue > props.propValue;
    return <span>
        Is over: {isOver ? "Yes" : "No"}
    </span>
}

So, you get a function instead of a class, referencing properties is a bit easier, and you don't necessarily have to worry about setting the correct state properties, because you can use something more akin to traditional setters from languages like Java. This also seems pretty good, right? Well, yes, initially it does.

Where it all goes wrong

However, this is not where things end. React also got the useEffect and useMemo hooks added. For example, the former allows introducing side effects within functional components. Without the ability to do this, functional components would still feel really restricted, whilst i'll argue that their present implementation can lead to more problems than they're worth. The main thing that has caused the most problems in my experience has been the approach to managing dependencies here, for example, you can write:

useEffect(() => {
    ... // side effect code
}, [some, other, variables]);

The above might not seem too problematic, but everything breaks down once you add more usages of state and have more than one effect within your component. This is the situation which inspired the entire post and was also the aforementioned problem that i ran into at work. For example, consider the following code:

export default function SomeFunctionalComponent(props) {
    const [foo, setFoo] = useState(...);
    const [bar, setBar] = useState(...);
    const [baz, setBaz] = useState(...);

    // you can also have a bunch of functions here, by the way, to add to the confusion

    useEffect(() => {
        ... // a whole bunch of code
    }, [foo, bar]);

    useEffect(() => {
        ... // a whole bunch of code
    }, [bar, baz]);

    useEffect(() => {
        ... // a whole bunch of code
    }, [foo, bar, baz]);

    return ... // rendering code
}

Now, in this example, can you track when the component will be re-rendered because of the side effects being reevaluated? Of course you can't! When presented with a >300 line source file, especially in TypeScript and with no comments to explain why each of the effects is separate, you'll probably find yourself rather frustrated and confused. Well, what if we add a 4th or a 5th effect to the already unmaintainable spaghetti code above? Things get even worse, yet thankfully you're really unlikely to ever make it that far.

It won't be long, until you'll see something like the following instead:

example-of-everything-breaking

So, React detected that the pattern of changes above makes the effects go into a loop, because each of the effect blocks actually are capable of calling the above setters and do so in practice. That does actually seem helpful, doesn't it? We're told what caused the problem, so we should be able to fix it, right? No, not at all! The problem here is that react knows that the problem is probably caused by a setState within one of the useEffect blocks, but which one? It doesn't tell us, which is about as bad as using JDK 8 or older ones, which don't give you accurate information about which exact variable caused problems!

But surely we should be able to just click on the source code and go to the offending lines of code, right? Wrong yet again! Clicking on the source will bring us to the React code, which caused this exception, instead of the code that's actually responsible for it by interfacing with React. That i think is one of the worst pitfalls within the entirety of React and also many of the other programming languages out there!

Now, do i have any concrete examples of this? Yes, but also no. Believe it or not, i ran into this exact problem at the aforementioned project in my dayjob, which was written in TypeScript and essentially broke all of the app whenever it was opened. It was an absolute blocker that prevented anything from working at all, however, when i sat down on a peaceful Sunday, utterly burnt out but at least intending to write an article about the problem and also how to fix it, instead i found that i cannot reproduce that exact problem when transpiling the TypeScript code into JavaScript, which instantly killed my hopes of being helpful to my colleagues and also fixing that annoying problem so i could instead get back to writing new features.

Summary

Because of the above, this article belongs in the "Everything is Broken" section, though perhaps it should be titled "Everything is Disappointing", because on one hand everyone seems to jump head first into using hooks while never having any clear reasons for doing so, apart from wanting to make their own CVs look more shiny, or refering to vaguely worded React blog posts, while on the other hand we don't even have proper debugging support within the framework itself to not only let us know that everything is wrong (that much is already evident), but to actually know where exactly the problem is and how to fix it.

So what should you do as a developer? Probably fight back against hype and keep your functional components pure in most cases, without any state at all. And then, if and when you do need state, consider using class based components, which will avoid this whole kerfuffle. But if for some reason you're stuck in a team with people who want to use hooks, strongly suggest that you should only use one useEffect hook per component, to avoid awkward situations like the above. Noone will ever have the test coverage to catch all of the situations like this, so your only chance is to nip them in the bud.

Update

Turns out that hooks, which were supposed to be entirely optional, have now infected the ecosystem to such a degree, that using class components simply won't be possible in many cases!

For example, look at the following code:

constructor(props) {
    super(props);
    this.t = useTranslation().t;
}

In practice, this leads to a problem and simply won't work:

ESLint: React Hook "useTranslation" cannot be called in a class component. 
React Hooks must be called in a React function component or a custom React Hook function.
(react-hooks/rules-of-hooks)

So the claims about hooks being optional were essentially lies, since their adoption and integration with the ecosystm forces them to be used!

For example, look at the official documentation for i18next. There, you'll find the following bit of colorful language:

There might be some legacy cases where you are still forced to use classes. ...

Forced to use classes? Only in legacy cases? This feels like some Newspeak from 1984, wherein any attempts at having a differing opinion are extinguished immediately. If you have any doubts about that, wait for 5 years and see whether they don't get around to deprecating class based components entirely.

In addition, perhaps consider looking at how many problems that have been caused by the functional components you can find online.