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.
I agree with this exactly. I think appeals to SOLID or any other design principles are generally not helpful when arguing/discussing about code quality. I think people should just talk about readability and maintainability instead, since those are what we are actually interested in - not following some dogma.
Those are impossible to measure though, would make a discussion extremely hard. SOLID principles are accepted as something that makes your code more maintainable over time
The first time I ran into some problems because some component looked stateless but in fact had state and the constant need to jump into components’ code after my team introduced hooks to our react front-end showed me quickly that I want out of the madness. I still can’t understand how hooks where so celebrated by the community. Such a bad idea for code maintainability.
I really wish Redux hadn't fallen out of favor so dramatically. In my experience a large proportion of the problems people ran into stemmed from the fact that the elegantly architected, unidirectional data flow model often had to mate with HTTP/REST backends, which fundamentally force you into a request/response model. This results in a combinatorial explosion of states and transitions you have to care about with respect to "in-flight", "error", "canceled", "received", etc. parts of the request/response lifecycle.
The other issues people had like boilerplate and massive reducer files I think were solvable with various conventions or tools, but forcing an interface between unidirectional data and request-response seemed like an intractable problem.
I've wondered for awhile if there could have been some way of extending the unidirectional flow all the way back to the server in a way that could solve this mismatch in a cleaner manner.
My understanding is that he simply does not recommend it anymore, and has put his weight behind hooks, which he's certainly free to do, but I can't really make sense of first seeing the upside of the model of Redux and then subsequently suggesting a tech that takes nearly none of the upside of Redux and runs in a completely opposite direction.
Even after learning how to use hooks the concept never fully clicked. The concept of functional components (smart components and dumb components) clicked for me right away and I've always preferred this approach. I guess the React team just traded one set of problems for another. In the end, when ever I've worked on a team of 3 or more, the code has never resulted as clean as I'd like (regardless of the approach). Adhering to to the architecture and design philosophy of the choosen approach (hooks vs functional components, etc) is more important as either can become unmanageable.
> The reason people started embracing functional languages was that, for the most part, they did away with state in favor of just passing functions everywhere.
No no no. They did not 'do away with state'. Handling state became _explicit_ rather than implicit. Haskell, for example, has _tons_ of different ways of handling state depending on the specific situation. State is a key feature of React, and the question is not whether to have it or not, but how to support it.
> OK, I just spent several paragraphs explaining why SOLID principles apply
No, you didn't. You explained to someone that already cares about SOLID that they can apply that thinking here. But didn't explain why I, someone that doesn't care about that acronym, should care in this or any other case.
> People complain about all the boilerplate in Redux mapStateToProps/mapDispatchToProps, but what that boilerplate does is give you a specific place to put the dependencies that is outside the View, so that the View can focus on rendering the Model and capturing user input, without worrying about saving user input back to the Model or exactly where in the Model the data to display lives.
A react component is a view function with some effects + local state. Whether you represent that with a class or a function + hooks doesn't change that. It's just a different set of tools. The principles don't change. You can architect your view function however you like.
> But you don’t gain anything from that hiding, because you have to read the code of the hook to understand what arguments to pass it and what it returns.
This is...just how programming works? Functions typically have documentation describing inputs and outputs.
> Now, let’s implement isAnyOf like a component with hooks
You're taking a simple function and turning it into a...component? What does that even mean? What dependencies are you talking about? What is the point of all this? It's just a higher order function.
> There’s a direct dependency running from App -> useDataApi, because App is directly reaching out and grabbing that dependency. There is no way to swap that out for a different implementation.
The component is a function. You can pass things into a function! You learn this in your first week of programming. What is the insight here? Why make this sound so complicated?
> And hooks set people up to create problems for themselves, in part because they encourage violation of those principles.
You haven't shown this at all. How do functions discourage abstraction? It's a choice by the programmer!
> I’ve noticed a troubling tendency in the JavaScript world recently, particularly in the React community. That tendency is for people to suddenly “discover” that decades of best practices somehow don’t apply to them anymore.
lol, this is just a problem with developers in general. Between burnout and hiring bias they trend young. Information and best practices struggle to get passed down across short generational overlaps.
The number of times I have seen the same bad ideas resurface over and over in my twenty year career is just insane. Document stores were the rage, then they were bad, then they were great again, and I think we’re cooling down on them again.
I have echoed similar concerns here before. I do think this problem is more pronounced in the web community than elsewhere. There is an unhealthy attraction towards new paradigms and tools. You need a certain amount of skepticism and cynicism to ensure bad ideas are resisted, but this community with its all embracing, unconditional positivity keeps shooting itself in the foot constantly resulting in an unstable ecosystem. The most important thing the community needs to adopt, in my opinion, is a sacrosanct approach to backwards compatibility. In the words of a very wise man, "WE DO NOT BREAK USERSPACE!"
As an example, I find the idea of deno really ludicruous. As if the ecosystem wasn't chaotic enough, now there is a new runtime, and of course it is not compatible with the current standard. While it's a pretty cool and inventive piece of work in itself, the idea of having to rewrite all of userland should frighten the community. People should challenge its need and value and push the community to find alternatives. People should respect the time and effort it takes to reach maturity. But now we now have a rewrite of node dotenv in deno dotenv, oak middleware inspired by koa, a new semver package, a new base64 package, yet another modern web framework... and what not. I predict, we will witness a race within the community to be the first to rewrite popular packages, node packages will slowly get abandoned, companies will open positions for deno developers, there will be a deno conf and new developers will be asking whether they should take the node course or the deno course.
Deno did pick a standard: the standard of the browser. Deno more directly aligns with the web browsers versus Node "standards" that were designed almost intentionally to be incompatible with web browsers. Most of those libraries you point out are all getting rewritten for ESM (a seven year old standard from 2015, mind you) and better browser compatibility anyway as Node itself finally slowly aligns with web standards. Just because they aren't all "necessary" rewrites doesn't make that all of them are "not useful".
The good news is that in this case the "competition" seems healthy: Deno's efforts have spurred Node forward to better embracing standards they've notoriously avoided before (or at least have been slow to adapt to). Deno's efforts have helped make Node more solid in its ESM support, as a big for instance. There's still an optimism that Deno and Node will again converge sometime down the future.
A thousand times this. People were so grossed out by Deno resolving imports from URLs... but browsers have allowed this forever, Deno was just aligning to how the web already works.
But React hasn't broken userspace, at least not in regards to hooks and class components. It is still possible to develop a React app today, from scratch, using only class components. Similarly, just because Deno exists does not mean that Node has stopped existing, or even being developed. As far as I can tell, "userspace" is as safe as ever.
Fwiw, I'm very glad that Linus himself hasn't taken your attitude to new technology, and is quite happy exploring experimental ideas, such as the new Rust-in-Linux work that's going on right now.
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.