The article suggests that HOCs should have been the "functional programming" path forward for React, but I wonder if the author had much experience actually writing with and/or debugging HOCs? Hooks didn't come out of left field and the growing number of HOCs and how hard they were to build/maintain/debug was a part of the impetus for "inverting the dependency injection" and building hooks. I don't even think the similarities in names was an accident.
Also, HOCs were really bad at SOLID too. They replaced inheritance with composition. They rarely supported Liskov Substition. Their dependency injection was often in the wrong places. They spit out regular dumb JS OO classes and object instances so their supposed object-oriented-ness wasn't directly in question, and that really was the only thing to commend them on.
That's a good tradeoff in statically typed language. I'll take composition over inheritance and abstract out the interfaces I want to maintain, rather than worrying about who has implicitly started to depend on an incidental interface.
This is yet-another-example of why JS has a lot of growing left to do. If a language doesn't require you to define types (and enforce them), you get these messy dead-ends that are used to rationalize even worse solutions.
I also don't think it is an accident that React starting truly "in house" embracing Typescript until after Hooks. (React was always Flow typed, so far as I'm aware, but the switch in "guiding" type systems at least in terms of public stances is much more recent, and I believe post-Hooks.) Hooks deserve a lot of criticism. They are absolutely a compromise even to their creators. (The "Hook Rules" seem to be something that the creators had wished to embody in a type system rather than a linter but didn't have the tools in current Typescript and the linter approach is obviously a compromise.) But they are well-typed in Typescript in ways that HOCs never were. Their APIs are entirely explicit and well-defined.
Those APIs are "weird" to traditional OO developers and OO purists, but those APIs are designed for static typing and accountability to type systems (Typescript especially now).
But a lot of the issue with HOCs was that they were so dynamic and difficult to properly type - simple cases would work but be very complex, and complex cases were almost impossible without dropping down into `any` types or type assertions somewhere along the way.
In contrast, hooks work much better from a typed perspective, but also from the perspective of providing composition over inheritance - it's much easier to compose multiple hooks to create new functionality than it was to compose multiple HOCs.
> Hooks didn't come out of left field and the growing number of HOCs and how hard they were to build/maintain/debug was a part of the impetus for "inverting the dependency injection" and building hooks.
I agree. However, while hooks solved one problem that you illustrated, it created another one: more and more business logic is tied directly to component lifecycles.
Further, in order to write tests we have one good tool: integration testing by setting up a mock API. I'm not saying that's necessarily bad but it's quite a huge lift as compared to passing props into a component and seeing what happens.
Yes, I think that's an inherent balance problem, and a criticism of hooks that I agree with: hooks make it easier to slice in other concerns so more concerns are sliced in that maybe shouldn't be. HOCs were hard and a pain, which kept them relatively infrequent. As a better replacement, hooks are sometimes in a weird spot of "making it too easy".
As other commenters point out the lament that Redux has mostly fallen out of fashion, where my own experience points out that Redux is still incredibly useful post-Hooks. (For testability, for separating concerns in clear ways in a codebase, and even for debug-time tooling.) Hooks are good at what they do, but what they do should probably not be your entire business logic of your app. Striking that balance is hard, different teams have different ideas and ideals. It may take a bit before "the best pattern" emerges from all this.
Sometimes I wish post-hooks that there was a good way again to spot "pure" components in the wild (entirely prop-driven with no internal state). Obviously, pre-hooks this was sometimes very easy to see in a codebase "pure" components were most often written as functions because functions had to be "pure" and stateful functions had to be written as component classes. Maybe less obviously that wasn't always a clean distinction either: there were plenty of pure components written class-style simply for habit, and there have always been ways in JS to sneak side effects into even "pure" functions, but it was a good enough first-order approximation that a lot of people relied on it, including myself. I don't know if there is a good way to mark "pure" React components today in a similar first-order approximation other than in documentation, though documentation is a good place to start and most libraries aren't currently doing even that and about the only reliable test is to grep /[\b]use/ through your project, and even that has some issues with not every hook follows the "useSomething" naming pattern, just most of them. (And the false positives that "useSomething" may still be the best name for even a non-hook function.) I don't have a good answer here, but I do understand it as a pain point of hook usage, and one that is a low level bother to myself too.
Also, HOCs were really bad at SOLID too. They replaced inheritance with composition. They rarely supported Liskov Substition. Their dependency injection was often in the wrong places. They spit out regular dumb JS OO classes and object instances so their supposed object-oriented-ness wasn't directly in question, and that really was the only thing to commend them on.