The entire "no side effect" aspect of functional programming is just a huge misunderstanding at best. It's unfortunate so many pushed that narrative. Many FP languages do not even restrict side effects. But those who do, like Haskell, do so in order to communicate where those side effects are taking place.
In Haskell for example you can put all of your code in the IO monad and just have side effects anywhere. This works fine. But you quickly realize that there are benefits to separating out code with side effects from code without. The types make this clear. Haskell provides powerful mechanisms to weave functions that both have side effects and those that don't with ease while maintaining that clear separation.
If anything FP in this manner is an extremely powerful version of side effects. It's not about "no side effects at all" but rather taking control of them and using them to our advantage.
> But you quickly realize that there are benefits to separating out code with side effects from code without.
This belies your whole previous argument...
Everyone understands that side effects are a requirement (literally - a program with no side effects is useless). Functional programming herds the programmer into a situation where code that creates side effects is consolidated into just a few places, and the majority of the code is pure functions.
That paradigm has a real cost - consolidating side-effects isn't particularly easy, and you have to work to do it.
But in exchange you get a LOT of pure functions that are
1. Easy to reason about
2. Easy to compose (because they have no side effects)
3. Easy to test
Hooks are the antithesis of this - they create code them seems pure, and has the guile of being composable & testable, but in reality they are very hard to reason about. They have completely undone the work of consolidating state and side-effects into one location. It is very easy to call a function with a hook in it in a way that breaks that function, and it's usually hard to reason about what subtle differences are causing this new breakage.
Some things are easy to express as functions (compilers). Other things aren't (user interfaces).
Even when immutable data is easy and is good from a software design perspective it is often a terrible choice from a performance perspective. Advocates say the performance loss is just a factor of two in many cases but that's why FORTRAN survived so long against C, why people are developing Rust when Java is available, etc.
There's a reason no on is writing modern games in functional languages, and that reason is performance.
But that said - At least for me - the major attraction of React was that it really concentrated on making ui related code pure. Give a component the same props, and you get the same DOM.
That's a really powerful concept for reducing bugs, easing testing, and giving you composable components.
It is not a performance improvement.
I think hooks really hollowed out the value proposition here. Because class based components were more painful, I used to see a lot of care and thought put into consolidating the logic that generated props into a single class based component (consolidating state). That component would then mostly pass down props to pure components.
Hooks make it easy to just throw state into any old component - which is nice in some sense, but like I said - it hollows out the value proposition of having pure components.
Good teams will still try to write mostly pure components, but many folks will just liberally scatter hooks into their code, creating code that becomes increasingly hard to reason about.
That reason is not performance but familiarity and ecosystem.
A trendy way in gaming to build games is to use ECS which is FP and there are very performant framework to do ECS.
I would argue (pretty hard) that the reason is actually performance.
The ecosystem matured around C-style procedural language concepts because naive functional implementations simply weren't fast enough (and were often much more difficult to work with).
Yes - some companies do leverage FP concepts for development, but they're usually heavily modified for that specific purpose (ex: GOAL at naughty dog, ECS for Unity)
And even then... ECS is "vaguely" functional at best. The entities are mutable, and the logic in the systems is directly modifying those mutable entities. I appreciate that the logic is applied consistently, and I think there's value there that comes from FP - but it's very much not classic FP.
> Hooks are the antithesis of this - they create code them seems pure
I disagree. The presence of a hook is the indicator that something impure is happening. Seeing a hook should be equivalent to seeing a promise, option, IO type etc.
Hooks also compose beautifully together. You can make so many great new hooks by combining just useState and useEffect together, bundling up that functionality into a new hook that you can then use in any UI.
> The presence of a hook is the indicator that something impure is happening.
Yes. And that's my whole point.
React was very powerful when care was taken to place impure code into a single class based component, that then passes state down to pure components as props.
React is a lot less powerful when developers scatter hooks everywhere.
New developers no longer have to go out of their way to understand the render lifecycles of a class based components, and feel the pain of writing componentDidMount or componentWillMount or componentWillUnmount or shouldComponentUpdate functions. Instead they just throw a hook in. Which is mostly ok - but it's hiding that you do actually still have to care about how this whole shindig works (and opens up a whole new world of pain around identity and equality checking, re-render cycles, dependency passing, etc)
I'm not saying hooks don't have an upside (ex: I'm right there with you, I mostly prefer a hook to an HoC from a reusability stand point) but hooks let developers shove their head into the sand and mostly pretend that they're writing a pure function - and they're ABSOLUTELY NOT.
There was nothing preventing you from scattering state everywhere in class based components. On top of this the component tree became a huge mess of HoC's stacked on top of each other.
You absolutely should not be scattering hooks everywhere in your code base. The same principle applies to use them higher in the hierarchy and pass down props.
This is a simple principle that can be taught to a new React developer. Keep your state at the highest level it makes sense to no matter the state mechanism used.
Hooks allow for composition of effects in a way that class based components did not.
> There was nothing preventing you from scattering state everywhere in class based components.
There was though - it's the same pain you're referring to later... "Hooks allow for composition of effects in a way that class based components did not."
Class based components sucked in a lot of ways. But the nice side effect of that was that folks tended to use them more carefully, and avoid using them when they didn't understand them (or at least avoid implementing any method besides render()).
I'm not saying hooks don't have nice properties - I'm saying that I'm not convinced (after using hooks for about 2 years now) that the price you pay is worth it.
The number one source of bugs in our codebase is... drumroll... hooks. I think a part of that is that state in general is evil, and will be where most of the bugs lurk. But I think the other side is that hooks have a completely new, unintuitive, hard to reason about set of rules. Composable? Sure, sometimes, if you work really hard to understand exactly what sort of new rules you're creating and then hiding in their complexity. Intuitive? Fuck no!
We can just agree to disagree then. I'd find libraries all the time on github which had class components using state in weird ways you wouldn't expect.
It sounds like your org could use some simple guiding principles and code reviews. You seem experienced, this shouldn't be a big problem. Maybe help guide your junior devs?
Eh - I'm not really sure we're even disagreeing. I just think that hooks let you stack the abstraction tower a lot higher, and answering some fairly basic questions can become really hard.
There's power there, and I absolutely agree that hooks do a better job of making for re-usable code than HoCs, I just think that the general level of understanding for them is low, and most devs do a really poor job reasoning about them (and in generally - I find they're basically impossible to reason about in isolation).
I see people do things like wrap everything in useMemo and useCallback, or pass complex objects to useEffect as deps, or fail to understand that making the output of useState the dependency of a useEffect hook that happens to call the corresponding setState function is a recipe for lockups, or any number of other fairly simple mistakes.
Plus... tools like redux strongly encourage destructuring semantics, and destructuring for hooks is absolutely the wrong thing (for the same reason - equality and identity checks). But then you're in a conversation about object identity and memory locations with a dev who has never encountered a pointer in their life, who's 6 months out of a bootcamp, and whose eyes are glazing over further and further with every word out of your mouth.
Worse - hooks can give you a loaded gun if you expect all the environments your code runs in to act like a browser (see my useLocation example with JSDom). Works a-ok when tested in a browser. Will even work nicely for the specific tests you might write for your component (since folks generally mock their hooks) but will absolutely foot-gun you if another spec calls the real hook. Happens to eat up a boatload of CI cpu usage and time as well.
I've had several conversations where fans of Hooks will justify them by saying that "functional programming is about composition over inheritance".
And I think that's entirely missing the point of functional programming. The goal wasn't to remove inheritance in favor of composition, it was to remove STATE - which in turn results in the nice property that functions can be composed, because they take all relevant data as arguments (they are pure).
Hooks basically blow that away - you've added back in all the problems of local state, but now you've hidden it behind a brand new paradigm that developers just don't have a very good feel for (even years after the introduction of hooks).
I'm reasonably well-versed with hooks, and even I find myself having to do incredibly complicated and deep dives into upstream code to answer simple questions, like "How many times will this hook run?" or "How many render cycles will this hook introduce?".
Sometimes the answer is so far upstream it's basically impossible to answer without running code - Ex: if you depend on the "useLocation" hook from react-router-dom, and you pass the entire object as a dep to useEffect (which is a mistake in and of itself), you will be fine in the browser, but Jest tests will trigger an infinite render cycle, because JSDom generates a new object for each call of window.location.
I can reason about functions that are pure, and that's the freaking point of functional programming. I cannot reason about functions with hooks in them - it's FAR worse than class based components in basically every way except ease of re-use.
I think in many respects - we threw out the baby with the bathwater.
> And I think that's entirely missing the point of functional programming. The goal wasn't to remove inheritance in favor of composition, it was to remove STATE - which in turn results in the nice property that functions can be composed, because they take all relevant data as arguments (they are pure).
I came here to see this said...regardless of the method used, state is what is challenging to maintain, regardless of how your framework or tool modifies and tracks it. And the only way I know of to properly wrap some sense of sanity around complex state modification is with unit tests, again, regardless of framework/tool. If you can't test it with a unit test, then you're going to struggle manually testing it as well, even if it does usually work.
A side-note is that I always thought the obvious split for functional/class-based React components was stateless/stateful (as full-blown objects are basically purpose-built for tracking state), so I was surprised when I joined this new project at my employer and learned about the interesting world of hooks. I rarely dabble in React however.
My snarky side today wants to add "developers struggle with maintaining state, what else is new".
I want people to be critical of the method du jour, but these 'production-like code examples' defy most of the modern conventions that a React dev would use. For example the fetch example seems to ignore its own prior destructuring assignment to make an awkward reference to an object property.
I agree, but the problem is that when you refer an object inside a hook, the eslint exhaustive deps will tell you to add that object as a dependency. In that example, it's easily solved with destructuring `const teamId = params`, but a lot of developers will blindly follow the eslint complaint.
> I do not exaggerate when I claim that I find a dozen of hooks-related problems every single week while reviewing code.
I also see sooo many issues when reviewing code like leaked event listeners, unstable props causing re-renders, etc. And these issues show up from teammates who otherwise write impeccable and trustworthy PRs in other regards.
I enjoy writing hooks style code, and for me reasoning about lexical scope & closures is second nature. But for many engineers used to OOP, hooks code is the first time they’re asked to do this kind of reasoning since leaving university. In OO Java/JavaScript, it’s very normal to declare a new class and have the only two scopes be the current function’s locals, and class instance members. Hooks code on the other hand can easily reach two or three layers of local closure scopes, each with different lifetimes. I think this is fun and clever, but I also prefer to maintain boring and simple code… I’m worried that hooks ramps up trivial complexity too much in exchange for often-unneeded power.
On the other hand, function components and hooks tend to guide people more toward splitting up their big mega components into smaller parts. At all companies I’ve worked for, product eng across the stack tends to produce mega objects cause it’s easy to just keep adding private methods to do one more thing, and splitting up responsibility of state encapsulation takes some extra reasoning. At least with FC/hooks, the API and linter force you to unroll funky logic or loops into sub-components, since you can’t call hook in loop or conditional.
> I think this is fun and clever, but I also prefer to maintain boring and simple code… I’m worried that hooks ramps up trivial complexity too much in exchange for often-unneeded power.
I've been in and out of the React world for the last 5 years, and this statement hit hard for me. Between the major shifts in best practices, the abstractions on top abstractions and constantly tripping up on the slight syntax differences between JSX and markup, so many commercial React apps I've worked on make me want to pull my hair out, and not just because of hooks.
In hindsight, the virtual DOM hype has not lived up to expectations, and I find newer frameworks like Svelte to be so much easier to work with. With the amount of React code running today, it's hard to see a future without it, but I'm so ready for it to be supplanted by something simpler.
There’s too much framework in Svelte for me; I don’t like feeling so estranged from “regular” code. I think SolidJS is more appealing - although it might be even more clever than React…
I haven't built anything with SolidJS, but it's not my style based on the docs. It is interesting that you bemoan being so far from "regular" code in Svelte when it's basically a minimal superset of HTML, as opposed to Solid, which uses JSX (which feels very far from "regular code" to me). Also given the fact that Svelte is a compiler that emits vanilla JS, I have a lot of more control over performance, compile-time checks for things like a11y, unused deps, etc. I'd argue that svelte source code looks a whole lot more like it's output than SolidJS does, but that't just me.
It's easily the best overall front end developer experience I've seen, but I've only built smaller projects with it so far.
Svelte feels very minimal to me. It is regular JS where I can say "shove the value of this variable onto the web page and rerender that component when the value changes" but saying all of that is just $:
Interesting - I began using Svelte after years of using React, and I found it to be far less framework-y. I feel much more connected to what's actually happening, but I also haven't built out a very complex application with it yet. Is it the weird $: reactive model that you don't like? That was the weirdest part for me, even if it's supposed to be "normal" javascript.
I had bad feelings about React when I first saw it. Then I saw RiotJS and that just clicked for me. I like Svelte too - haven't put it in production tho
I have to say, I've never felt this in the Vue ecosystem. It's relative simplicity in state management, and slower relative paradigm shifts have been pleasant. The 2->3 shift wasn't perfectly done (and it did split the state management stories), but overall I've felt like I "kept up" fairly easily, compared to React.
And that's while writing a lot more React than I did Vue.
> I do not exaggerate when I claim that I find a dozen of hooks-related problems every single week while reviewing code.
I’m curious if the author sees dozens of problems every week in code review due to, say, unexpected null/undefined values, mistyping variable or attribute names, using the wrong number of equal signs, failing to catch errors and handle rejected promises, etc.
I'll give you my answers to your questions as someone writing production JS/TS. Answering your questions is precisely illustrative of why hooks issues are hard to catch.
unexpected null/undefined values
This is an issue and has led to bugs. Switching to TS and making the compiler flag them fixed it.
mistyping variable or attribute names
Not an issue. Code obviously breaks if you have incorrect names.
using the wrong number of equal signs
This is an issue but causes few bugs. Linting generally catches it, though cute boolean punning still bites us.
failing to catch errors and handle rejected promises, etc.
This is an issue and has led to bugs.
Pretty much anything where there's some implicit details that the compiler or linters can't reason about programmers find a way to get wrong. One thing I like about the hooks linter setup is that what it encourages you to do by default will prevent most bugs, only lead to potential performance issues, unnecessary rerenders, unnecessary refetches.
>> failing to catch errors and handle rejected promises, etc.
> This is an issue and has led to bugs.
> Pretty much anything where there's some implicit details that the compiler or linters can't reason about programmers find a way to get wrong. One thing I like about the hooks linter setup is that what it encourages you to do by default will prevent most bugs, only lead to potential performance issues, unnecessary rerenders, unnecessary refetches.
This is something that can be mitigated via the no-floating-promises linter rule if you're using TypeScript[0]. For the cases where you actually want to just swallow errors, you can just add a `.catch(noop)`. This makes such situations explicit. You can get even stricter with the no-misued-promises rule[1].
Thanks for those references, I've recently been handed an overwhelmingly "JavaScript" project and this'll be a great way to expose some overlooked code paths.
The article here ("Hooks Considered Harmful") actually probably would be better called "Most Common Mistakes When Using Hooks". It seems to cover the most common ones I'm aware of.
Don't have a blog post, but the majority of issues you will find are with dependencies arrays for useMemo/useCallback/useEffect.
Most common I see:
1: Missing dependencies (for folks who don't use the linting rule)
2: Not understanding reacts async state model: example being expecting state to have been updated immediately after calling setState inside an effect
3: Not understanding closures: an example is creating an interval in an effect that uses and updates a useState value, but being confused why it isn't updated when the interval repeatedly fires.
I agree wholeheartedly with the author. Hooks are powerful and even I was super excited to start using them when they released. But now I think the hooks paradigm leads to even worse and bug-ridden code than what we had before.
The docs are pretty good, but don't seem to cover very well that when using hooks you really need to know JS well, which includes equality in JS, closures, etc otherwise you will be guaranteed to shoot yourself in the foot.
There is so much more business logic mixed in with component code at seemingly random in projects as well such that it makes finding where "stuff" happens even more difficult than before.
> I also see sooo many issues when reviewing code like leaked event listeners, unstable props causing re-renders, etc. And these issues show up from teammates who otherwise write impeccable and trustworthy PRs in other regards.
I think this is true. However, I don't think I saw less bugs with `component(Did|Will)(Mount|Update|ReceiveProps)`. Lifecycle events are intrinsically about state management, and that has always been the root cause of many bugs. This isn't a React specific problem either. Back in AngularJS 1.2.x days, the $scope and digest cycle was the source of many bugs. In Backbone, people's two-way binding between views and models were the source of many bugs.
Are React hooks complex? Yes. I don't think they're worse than what existed before though.
I don't entirely agree - the old React way was verbose and explicit about when events are executed. This allowed developers to more concretely reason about logical workflows in applications.
With hooks, we traded verbosity for a single interface that does it all (assuming you know how to hook up your dependencies correctly, or compose helper hooks to manage state comparisons). Hooks allow you to do mostly anything lifecycle methods did, but they're a lot trickier to reason with, review, and develop.
This all goes away if all your developers are functional maestros - in practice, it's lead to buggier code across our various frontends.
My experience has been pretty different. With lifecycle methods, you had the implementation details of a single concern split across the constructor, multiple lifecycle handlers, and sometimes even the render function when refs were involved. Hooks can express full concerns in a reusable way. This is a valuable abstraction that previously required complex higher order components to do.
People would also constantly get tripped up over `this.props` and `this.state` when it came to computed state values. Now a simple `useMemo` simplifies and expresses that intent way better than setting something conditionally in the constructor and doing a bunch of conditional checks on componentWillUpdate before calling this.setState again.
Edit:
Oh, and the improvements with Redux are life-changing. The `useTypedSelector` UX is so much better than writing a mapDispatchToProps, mapStateToProps, and then having a bunch of merging ceremony there.
I'd chalk it up to a difference of opinion then - I like simplicity, but I'd take verbose clarity over it any day. Having to explain to newer engineers the nuances of hooks (and the permutations required to wrangle them in) is harder for me than saying "this exact lifecycle method is what you're looking for, take a look at its documentation".
Chalk it up, then. I've never had a problem explaining the nuances of hooks, and there isn't actually that many permutations needed to wrangle them in indeed - but our engineers are usually quite quick to pick these things up and understand the interface without having to worry about the implementation anyway.
It is a challenge for many developers. It’s taking developers I work with between 2-5 months to really grok it.
So if you’re working on multiple tiny changing projects, or with contractors who are only gonna be around for 6-12 months, it’s possibly not worth it.
But if you’re working on a project that needs to stick around, and the people you’re working with are colleagues who will remain in the company even if not on the same project, the training is totally worth it, IMO.
Thank you, glad to hear that our React-Redux hooks are useful!
And yes, the fact that the hooks work so much better with TS than `connect` was one of the major reasons why we now recommend the hooks API as the default.
After dealing with years of the hoc stacking hell, hooks makes the logic sharing problem so much easier. It's like switched from callbacks to async/await. I am not sure if there are better solutions.
Alright so, reading through all this I think I can summarize it:
- Hooks are tricky because you need to pass them an array of dependencies, which is manual housekeeping
- You shouldn't pass anything but primitives to a hook's dependency array if at all possible
What is the alternative? Just pay attention to the two above, or go back to class based components? Or will there be a React-flavored JS/TS (like JSX / TSX) that has different closure mechanics?
1. State you read in a ~hook~ aka Composable is/should be an instance of a special kind of observable object. You can create your own subclasses or just use the standard state container much like useState. This means the system has more information to produce minimum subscriptions/reactions at runtime.
2. The system used crazy compiler transforms to turn functions marked @Composable into reactive scoped hooks/components. Using the compiler eliminates a lot of error-prone boilerplate and bookkeeping code otherwise required for these kinds of systems in standard OO languages without monad+do-notation by adding a sublanguage “manually”.
Downside to the Compose model is that it’s even more mindbending to understand. Developers are encouraged to surrender to the magic. I’ve yet to read/write enough Compose code to understand the cost benefit analysis yet.
Is SwiftUI pretty much the same in this regard? I’ve only taken time to do the trivial examples in either Compose or SwiftUI, but they feel very similar, so I’m wondering if your prognosis also applies to SwiftUI.
I think the Swift compiler does less magic for SwiftUI. They added a new literal syntax for the UI view tree literals, but other than that I believe the behavior is in the runtime. SwiftUI expects you to use Combine, Apple’s (F)RP system, to a greater extent than Compose expects you to use RxJava/Kotlin Flow. My impression is that Compose is more React-y and is actually an escape from RxJava FRP-land; I can translate most hooks to Compose after reading the Compose docs a few times but find it much harder to do the same with SwiftUI/Combine.
Personally I prefer Compose because it’s open-source (so you can figure out how it works if you need to), has much better docs, and seems less welded to FRP stuff which I don’t enjoy. I don’t really have enough experience to really review either though.
Was there anything wrong with class components? It's what I learned half a decade ago, and the idea of a "state" object made so much sense. Now, with hooks and whatnot, it seems like React is trying to be "functional" without actually being so.
I’ll prefix by saying I agree with a lot of the criticisms, and I’ve experienced first hand how hard to explain and error prone hooks are. But for this post, let me defend the concept.
The idea, in a very rough nutshell, is to allow separating behavior and presentation.
Hooks are the reusable unit of behavior. You compose hooks into more complex hooks that might implement loading and saving data from/to the server, for example.
Then you can use this hook with different components, or use the same component in a different context with a different data source. This can be very powerful if used well.
But as I said at the beginning, hooks are unfortunately also very difficult to get right.
To expand on this, many of the things that hooks now make easy were bespoke per-class implementations with details that leaked into every lifecycle hook. Think of how you’d write a chain of useMemo calls in a class component to see just how bad it was.
I also liked/like the idea of one state object. With hooks now they really discourage packing everything into one state object and you either have to use a bunch of different `useState` statements for your different variables or pack a big state object in one `useState` but then you face big performance issues with needless re-renders when you only change one element of the state object.
With class components, lifecycle management is trickier to encapsulate and reuse since you have to split such things up and scatter them across several different methods or wrap the whole component in a HOC, and in general class components require more boilerplate. Thus you'll usually end up with code that's just a bit more spaghetty than with function components. With hooks, you can at least encapsulate some specific behavior behind exactly one function call without jumping through any extra hoops.
I would say mixing state and behaviour was the main problem with class components (and oop), although it might be a better problem to have than all the approaches supported simultaneously (some code with class components, some hook heavy, etc)
State wasn't evil, the lifecycle callbacks were. I don't think they ever deprecated setState, but I'm pretty sure they've nudged people away from the lifecycles..
Class components when needing more complex behaviours related to lifecycles/context had the choice of:
a. Hard wiring up all the different lifecycle events to bespoke systems, on a per component basis (use same thing in 5 places? Implement it 5 times)
b. export const ActualWorkingComponent = HOC(HOC(HOC(HOC(NonWorkingWithoutHocComponent,{conf4}),{conf1}),{conf2}),{conf3});
I will fully admit that useState is more aimed at singular/simple values, so a more complex object is a pain to directly copy over. (useReducer or wrapping setState could work, as the child components shouldn't re-render unless their props change).
But it is so much nicer to have the dependencies at the start of the the functional component, and not in a horrid callstack at the bottom, with 3 different ways of defining which prop gets which HOC's values/functions and then injecting even more callbacks to munge those values from the out HOCs to use in that HOC to make it's output good for next HOC...
It’s often not even that much manual housekeeping. If you follow the second advice, then ESLint will do a fine job of telling you exactly what’s missing or superfluous.
(I argue that ESLint is almost required when working with React. You can turn off its weirder other rules if they become an annoyance, but the hook rules are golden.)
No, I think the problem is the combination of closures, mutability and identity. Very few other things in programming punish you that harshly (and subtly) if you don’t have a crystal clear understanding of all three concepts.
ESLint is fantastic, but what would be more golden is unit tests failing or code not compiling. It is almost like hooks need to be written in a meta language like JSX so that they can be compiled and thus failed when written poorly.
> What is the alternative? Just pay attention to the two above, or go back to class based components?
React may have limited options with this design, but other frameworks have taken other approaches to the problem:
Vue/Svelte/MobX only run the setup code for hooks (or closest equivalent) once. Derived values and effects are automatically run without specifying dependencies - the tools detect what an effect reads while it runs, and track dependencies for you. Since effects are only set up once, closure values from the setup scope don't expire/disappear, so they can't go stale in the same way as in React (caveat destructuring). I think Solid is in this camp too, but I haven't used it.
Frameworks like Mithril and Forgo ditch state tracking and effects entirely. You explicitly tell the framework when to rerender etc., and everything else is just you calling functions and assigning variables without the framework's supervision.
Crank.js extends the explicit-rerender idea by using generators for components. This preserves the "a component is just a function" feature from React, but avoids the hooks pitfalls by only executing a function once.
Hyperapp doesn't have the notion of components at all, so you can't have component-local state. The framework reruns all view code at once, passing the current global state. You can approximate components by writing functions that slice and dice state and divide up the work, but that's transparent to the framework, and there's no place to store state besides the global state.
These all have trade-offs. They may require more complex runtimes / toolchains, or simply shift around the burden on the programmer (what's easy/hard, what kind of bugs will be common).
I'd love to see more approaches in this space. Not all trade-offs are right for all situations, and I'd like to see more ideas that meaningfully change the development experience, rather than "if you squint it's basically the same thing" ideas.
> I think Solid is in this camp too, but I haven't used it.
Correct. Solid is all about signals (reactive values). When you run any effect (rendering updates are effects created behind the scenes for you), it will get run once immediately, tracking which signals where called. Then it will subscribe to those signals to re-run the effect on change, and it will resubscribe to newly called signals, and unsubscribe from no-longer called signals.
I believe that it is roughly equivalent to Vue's reactive api, except that rather than using a proxy or setters to allow object mutation to trigger effects or re-render, it uses separate update functions, more like react hooks do.
I think most specifically the call out in the article is: the Hooks that React provides are extremely "low-level" (and intentionally so) and while you can do everything with just raw low-level hooks, consider returning to higher level hooks. The article provides some simple (Typescript type-safe examples) of higher level hooks. It also recommends that Redux/MobX/Relay/etc are still very useful higher level tools, even or especially in hook-based components in React.
Unless I am missing something, functional components and centralized state (a la elm https://guide.elm-lang.org/architecture/) are enough. Bringing global state (Context called from anywhere) can make it less clear.
Kind of neat how SolidJS solved these issues: each component function and hook runs only once.
There is no need to worry about dependencies since it won’t need to run again! Hooks like useState() do not emit a value — they emit a “signal”, a function that returns the current value.
Unfortunately there’s no react-native equivalent and the ecosystem is much smaller, but I have to imagine the React team has their eye on this alternative strategy.
The difference between observer approaches like SolidJS or MobX (and I'd also put Svelte in this box) and React's data-flow centric one is one of explicitness. With the observer approach change tracking is more implicit, i.e. embedded into the values you are using and the functions that are using it. Which does fix the problem of forgetting to declare dependencies, because using == declaring.
Now what it does not guard you against *per se* is unnecessary re-runs, I am willing to bet there are tricky cases with tracking of nested objects and updates based on partial changes there. SolidJS does expose various tools for untracking and batching signals, so it might be a matter of trading initial explicitness/complexity for adding it later.
This might be the right trade-off and untracking might be the smaller problem. But that feels like a somewhat team and product specific question. I think it is clear that without state libs React is to barebone to handle most somewhat-complex interactive apps, and a lot of them are observer based (MobX, Recoil, ...). But I do not see it as a silver bullet just yet.
> SolidJS does expose various tools for untracking and batching signals, so it might be a matter of trading initial explicitness/complexity for adding it later.
This is correct, but it’s exposed specifically for cases where you know more than the compiler does. This set of cases will almost always be smaller than similar cases in React, because ultimately Solid components are just functions. They don’t have a lifecycle, components themselves never rerun unless you call them.
Those cases for React: you have to tell it when it should rerun or not rerun your components, your logic etc. Basically everything on the event loop that isn’t already participating in its reconciliation algorithm, and everything which has its own diffing logic.
Those cases for Solid: likely interacting with other libraries with implicit lifecycles. Your event handlers will all run exactly as defined. You’re already invoking the signals and other logic which reconciles Solid’s state model. You just need to really mean it when you “get”, and libraries which aren’t aware of that need a little nudge.
You can get around this identity problem by creating all derived/composed objects via `useMemo`. This ensures that their identity only changes when that of their dependencies do. This lets you get around this "identity problem", but comes with some issues:
- Relying on `useMemo` preserved object identity assumes a semantic guarantee, which React docs tell us explicitly not to do [1]. Not providing this guarantee is ridiculous. If their cache is implemented correctly, this should be no problem.
- The alternative is to leverage an external lib, which does provide this guarantee [2]. However, it's weird that bringing in an external lib as the more "correct" solution to this incredibly common problem (this is seriously relevant to like 1/2 the components I write)
- Wrapping every bit of derived state in a `useMemo` hook is incredibly verbose and annoying, especially when you take dependency arrays into account. I feel like I'm writing generated code.
Identity is necessary if you want to predictably trigger `useEffect` callbacks.
Example: if you're not using a smart intermediate layer like `react-query`, you can unintentionally trigger loading states and re-fetches if you're not closely watching dependency array identities
Though this is also a strong reminder that though useEffect can entirely replace service layers and state management layers such as react-query/Redux/Mobx/Relay/what-have-you doesn't mean it necessarily should. (Ultimately that's the bottom line summary from this article.) useEffect is a very "raw" low-level tool, at some point it is a good idea to consider a higher-level tool (maybe one based on useEffect under the hood).
Don't forget too that trying to do everything in raw useEffect code may be a sign of putting too much business logic in your views and abstracting that out can be a good separation of concerns no matter how you decide to abstract that service layer (and/or which tools like react-query/Redux/Mobx/etc you choose to make that easier).
Disclaimer: I am working on a project which does not use hooks (or React or anything like that), but has a fairly complex set of data processing specifications. The project is > 10 years old, the project is a product in the sense that it has direct end users, but a library/framework in the sense that its behavior is also defined by end users (it’s not a UI around arbitrary spreadsheets, but that’s a pretty good common frame of reference). Most of these questions are informed directly by work I’m actively doing, some by past work on distributed systems and UI/UX.
- What about anything computed from the previously fetched data? Will it be computed the same way?
- What about any user-provided state downstream? Will it be preserved? Will it still be valid?
- What about any user-provided state midstream? Even if preserved, will it evaluate the same way after a refetch?
- If you know mid-/downstream user input might be impacted, can you detect that and ensure each case has a desirable outcome, or does this responsibility spread to all of those cases?
- What about inconsistent network connectivity? Will it fall back to the previous state in case of timeouts? Is it even supposed to? (Is the request idempotent? Do you know? Can you know? If it’s not idempotent, will it recover after a timeout once network available resumes?)
- What happens if user/event/timer-caused state changes while the request is in progress? How will computations be reconciled?
- What happens if network-provided data is also supplied by user input from other users? Do you have a reconciliation strategy?
- What happens if this first request triggers N requests? What happens if each of those N requests similarly has to answer all of the above questions?
- What happens if any one of these has a pathological case which causes it to cycle? What if it causes a cycle intermittently?
- What if your user is using the cheapest mobile available and has an expensive data plan?
- What if everything is really fast, actually, and your user has motion sensitivity?
I’m just rattling instinctive thoughts after stumbling on this comment. There are surely more I could come up with if I were actually dealing with concrete problems where unexpected redundant network requests are being evaluated as “is it more than a performance issue?”
One, you don’t rely on a semantic guarantee if you use useMemo for derived state. Avoiding rerendering counts as an optimization as far as the React docs are concerned (your program works if there’s an extra render), and this is in fact exactly what it was intended for. The docs you linked seem to agree: Regardless of whether an offscreen component keeps or doesn’t keep its useMemo, the code is correct and there’s at most one extra render.
Second, while I agree with the verbosity complaint, I personally make a point to use useMemo as coarsely as possible. It’s often completely fine to compute all derived state in a single big lambda that returns one big (immediately destructured) object. It’s only when you have multiple pieces of derived state that update individually and are also all expensive to compute that you actually need fine-grained useMemo calls. And in this case, you can always think about extracting sone of that logic into a helper function/hook.
It’s not perfect, but I think it’s possible to avoid a lot of the pain most of the time.
I'm with you on thought #2. Regarding your first thought, however: if you want control over when `useEffect` callbacks fire, identity isn't just an optimization, it's a necessity. For example (used in another comment): if you're not using a smart intermediate layer like `react-query`, you can unintentionally trigger loading states and re-fetches if you're not closely watching dependency array identities
Oh, that’s a good example. I’ll argue that you should try to use primitives (strings/numbers) as keys in those cases. But if you can’t, then you’re right that identity is critically important.
I'm curious if you've used useDeepCompareEffect, the use-deep-compare-effect npm package? I've found that it is pretty reasonable foolproofing for many of these identity questions. I'm well aware of Dan Abramov's objections to the deep equality checking [1] but I still find it a bit easier for me and other devs to reason about when doing things like data fetching.
Crazy how many choices there are in the mix (use-deep-compare-effect, the `JSON.stringify` approach mentioned by Dan, `useMemo`, and `useMemoOne`). Feels like a "pick your poison" scenario, as each one has a significant issue.
That being said, `useDeepCompareEffect` does seem the most "foolproof", and "foolproof" is probably more important than intuitive or performant in most cases.
Maybe this author doesn’t understand the history of the term “considered harmful”? The points are valid, and there’s lots else to be wary of in hooks, but the title shows a lack of experience.
The main problem with hooks is that it takes a lot of somersaults to do simple things. Just like class components, there are things at the core that are not kept simple. which makes hooks very complex.
I am building my first non-trivial Next.js app now. It definitely took a couple days to get a simple "fetch data from a third party and render" use case working. And even now I'm not sure if I'm holding it right.
I had a Next.js project due for an interview and it basically took my entire weekend, even with a working prototype in another framework. The interviewer later on told me that the assignment was his way of experimenting to find out if the company should switch stacks. I told him it was the worst experience I've ever had coding.
I was on Google's Web DevRel team for 6 years. Owned all the Chrome DevTools and Lighthouse docs for 4 years and led content strategy for https://web.dev and https://developer.chrome.com for 2 years. Lots of experience building small applications in vanilla HTML/CSS/JS and I build toy apps in whatever frameworks are currently popular. I'm a technical writer by trade so I usually don't have time / motivation / business rationale to dive into a particular framework to the point of mastery but definitely am not a novice either!
This comment [1] describes exactly how I shot myself in the foot w/ hooks. In my head this is how React talks to me now: "Tell me your dependencies. NO NOT THAT KIND OF DEPENDECY!!"
The main problem is that the hooks provided by React by default are low-level primitives of an incremental computing DSL, and you need to have a very good understanding of both JavaScript and the semantics of this DSL to work with those primitives correctly; it feels like having to reimplement strcat every time-- with every pitfall that you might fall into while reimplementing strcat in C.
Maybe this could be solved by a hooks "standard library" that provides generally useful hooks like useTimeout and a useMemo that is actually stable as mentioned in @purplerabbit's comment.
Bugs caused by faulty equivalency-related code have been common for a long, long time in React development. I couldn't guess how many bugs related to object destructuring I wrote, and fixed, long before hooks existed (although I can confidently say the former number will be significantly higher than the latter). I used to see these all the time on large Redux/React codebases. `mapStateToProps` was always a fun place to find them.
I agree with this article, when I’m doing frontend development I much prefer to use Vue - with its options API (I ignore the composition api as it’s essentially a clone of reacts hook system).
But when I do any react such as assisting another team using it, I am constantly surprised and reminded just how much of a bad developer experience react hooks are.
The coupling of needing to constantly be aware of the intricacies of rendering while also balancing reactivity and data binding leads to probably the most offensive API I’ve ever had to use.
When writing useState or useEffect etc the not always required last argument is a damn array or empty array or sometimes an array with several items.
What does not passing an array mean? Well, I’m sure someone could explain at great length the complexity and alleged need for this but anyway: passing nothing as the last argument of useEffect results in an infinite loop.
Passing an empty array means “don’t render again until actually needed”.
The third option of passing a variable length array results in something else entirely I still don’t understand. Every article does it slightly different.
Ultimately what this leads to for me at least is a developer hostile API full of seemingly intentional foot guns. The other day I had the situation where useEffect to call an API resulted in calling the API endlessly and constantly. Then I thought “ah I have seen this before, it means I have to pass an empty array”.
It’s absolutely infuriating and smells of a bad API design. What happened to the importance of DX (developer experience)?
As I said I’m sure someone could hand wave away the alleged need for this complexity but honestly it simply feels like a bad API. No one will ever convince me that an empty array vs no argument is “functional” because functional code values making side effects and purity deliberate decisions with a focus on clarity.
How do other frameworks let you fetch from an API? Generally a simple assignment and await statement.
I agree. Hooks are an awful hack / offer poor user (developer) experience imho.
Whenever you need to have warnings and rules for using things (that require linter verification to make sure developers aren't shooting themselves in the foot with common/regular usage) it's an anti-pattern.
They are far too easy to mess up, especially for something that is meant to be a fundamental part of the library.
I've been using SolidJS for some personal project and they use most of React concept but avoid pitfalls by running once the code of each component. Also, no need to explicit dependencies! The framework figures it out automatically. Really nice dev experience.
No, "state" doesn't mean "keeping things around while I compute other things". That's just intermediate results. State is defined by its ability to change as the computation runs; and the changes can affect how the computation unfolds. That's why it's "evil": uncontrolled state changes are difficult to reason about and are great cover for bugs.
I know this might be a bit off topic but I wanted to mention it: FactorialHR had an forever free plan[1] which they killed and forced me to go onto a paid plan. This happened after I recommended them to a lot of founders which then backfired on me (albeit a bit - ppl still trust my opinion). Needless to say all those startups migrated to a different HR provider cuz they just couldn’t trust a company like that with sensitive data.
But it’s just the way they did it and how after that there was a complete ghosting on my requests and queries.
> Most of those issues never manifest to the end-user, but incorrect code that is not a bug today will, eventually.
No... what? Could not disagree more with this sentiment; this is the "for the love of the game" kind of stuff that completely loses focus on why we write code in the first place; to make money. Very, very few people have the resources to care about this level of problem, and far more often people who don't have the resources end up spending them on useless "improvements" like what's discussed in the article, rather than building things that users can use (or that make it easier to build things users can use).
Hooks are surely flawed, in the same sense that literally everything in software is flawed. The point is not to select a way of writing code that doesn't have flaws; the point is to select a way of writing code that has flaws you can live with.
Hooks have flaws, but hooks also have benefits that make writing software meaningfully easier. Losing sight of that is a great way to write an article that complains about problems that never impact the user's experience.
It's just such a big red flag when someone talks about "incorrect code" that doesn't impact the user in any way. Huge, gigantic waving red flag.
Maybe I expressed it incorrectly. When the code is incorrect, it fails in the future. So the user will notice it. This happens when your code relies on some context to work (for instance, if it relies on how the data structure was allocated somewhere up the component hierarchy). When the context changes, then that piece of code starts failing, therefore impacting users.
But now it just sounds like you're saying, "When the code changes, the code changes." which feels obvious?
Of course when you change some of the code without changing other parts that rely on the original code, it will break. I'm not sure why that's unique to hooks.
I work in an infra team in a large codebase for Microsoft. We are the people who do the sort of bug-analysis to figure out what technologies work well and what needs improvements.
Hooks are a constant area of struggle and people make tons of mistakes (forgetting to cleanup a useEffect, useState closures, and needless useCallback are the top 3) with it.
I dare say that if we didn't have MobX things would have been much much worse.
The annoying bit is that other frameworks like Vue or Solid have MobX like Reactivity baked in which makes things much much simpler.
How does Mobx help your code work better? Does it specifically salve hooks wounds, or just because you don’t need to manually manage subscriptions in a component?
I’ve looked at Mobx and am concerned about mutation. I would like a magically reactive state container that always returns immutable views of the state for local usage. Maybe I should stop worrying and switch everything to Mobx.
Because you don't need to manage subscriptions, there's a whole class of hooks you don't need to write in the first place. Then on top of that, you get extremely good performance. Having to rarely think about hooks and basically never think about performance, for me, free's up tons of cognitive space to focus on the actual implementation of the UI. Its not a perfect system, but I haven't found another I'd rather use.
I only wish Mobx's API was terser and more opinionated. Hard to derive a good Mobx pattern from their bloated API (>100 exports)
I've created a wrapper library which is basically a more-opinionated, higher-level version of Mobx if anyone is interested: https://www.npmjs.com/package/r2v
I was curious and indeed confirmed that ember.js/glimmer.js @tracked properties were inspired by MobX.
From /u/wycats "The rule in Octane is: "mark any reactive property as @tracked, and use normal JavaScript for derived properties". That's pretty cool!"
One of the great things about MobX is that it works on plain data and plain operations on that data [0], not just the contents of a sanctioned Store, which means it has no opinions about where you put your data. Local objects captured by closures? Cool. Classes? Great. Mutating arguments? Go for it. Global singleton? Have fun.
[0] Technically, it wraps all the different data structures in Proxies etc as soon as they get assigned into an observable structure. But the goal is for you to never have to think about them as anything other than plain data, and that abstraction very rarely leaks.
It is! It can be overkill for very simple React apps, but I used it to build a power-user tool a couple years ago that would have been impossible to do with Redux or hooks.
Hooks are never the solution for state on their own. They're more for localized to a single component state.
That's why I brought up Relay (this is not the same as React, but works with React), cause it's what FB uses to stitch together global state between hundreds of different sub-components.
That being said, Relay does NOT have a simple object interactive state ability. It's an obscure "external store" with weird querying and mutating rules that can feel quite difficult to work with at times.
If Solid or Svelte had a way to import React components (for 3rd party code) and a documented migration path for 1st party code I'd be interested in switching.
For me it seems React has made some unfortunate choices in an attempt to go FP in a language that does not afford it very well. Instead of embracing JS and working in alignment with its limitations/offerings, the React team has introduced elaborate workarounds that gives a whiff of FP but introduces indirection and verbosity and does not solve basic problems of state (i.e. the example from the blog post with classes vs. closures).
I prefer frameworks like e.g. Svelte that works with what JS affords. If you want to go full FP Elm or ClojureScript will give you more value for your money than React imo.
FP is a wide spectrum and JS can do some FP well. I'd argue the issue with what React is attempting with hooks is not that it is FP, but it is an attempt at particular "higher-kinded" monads when JS today mostly just has basic support for simple kinded monads (Promises/thenables and async/await syntax). The types of monads that hooks want to be are advanced and require a bunch of extensions even in "big FP" languages like Haskell. (In theory, if the biggest concern was no do-notation in JS, you could approximate it with async/await syntax. The mental model of async/await might not make sense to what you were doing with it, but the dualism with do-notation should be valid. What Hooks are trying to be needs things like GADTs and other extensions in Haskell from my understanding.)
To me, I still think Hooks are useful for making that big swing attempt in JS despite needing so many crutches like hard education spikes in the learning curve and so many linters, but the problem with Hooks is definitely not that they are "FP" but that they are advanced FP that even FP still hasn't figured out all the bugs.
Not sure what you mean by higher kinded monads but hooks are kind of like dumbed down Arrows.
You'll see some similarities with the haskell libraries auto and netwire.
I'm not a fan of the pure JS implementation the react team did, I wish they went the compiler way like solidjs did.
It results in an API that is close enough to react and works faster and without gotchas (or hooks rules).
The FP term of art I was forgetting yesterday was: algebraic effects. Hooks as a whole are trying to model algebraic effects. Algebraic effects are still raw and new in a lot of FP languages. They are hard to reason with even in research languages designed entirely for testing algebraic effects work. The one time I saw an example of trying to model algebraic effects in Haskell it used a half-dozen or more GHC extensions including GADTs on top of monads. (GADTs applied to normal types create higher-kinded types. "Higher kinded monads" was a bit metaphorical, but not the worst description I could have come up with for how algebraic effects looked to me how I saw them modeled in Haskell that one time months ago.)
I wish what React was trying to do was just dumbed down Arrows or Observables. From what I understand of how things like React Concurrency and React Suspense are built to work they are doing a lot more reasoning under the hood about the hook effects than just the raw arrows would imply. I think it is an interesting big swing. I also realize that using ideas from way out in Research Land in a non-type-safe language is a huge risk and liable to create a lot of footguns (as it has).
> I'm not a fan of the pure JS implementation the react team did, I wish they went the compiler way like solidjs did. It results in an API that is close enough to react and works faster and without gotchas (or hooks rules).
I have mixed feelings on this. Pure JS has some advantages. While hooks as they exist offer some footguns, they also allow for some flexibility when you need it (sometimes, very rarely, you do need a closure around a local variable that is not intentionally in the dependency graph). Admittedly right now a lot more people are likely to succumb to footguns than need the flexibility. I think some of that is a balancing act that hooks and especially useEffect are very low level "assembly language" in React and the impression I have is that they mostly were never entirely meant to be used to the extent of writing say 100% of one's business logic in these low level hooks and instead were always intended to be lego building blocks in "higher level" hooks. (Related in part to how in React you aren't likely to build entire components' VDOM by hand in JSON notation, you'd probably use JSX or TSX.) Right now the React community maybe isn't using or building enough "higher level" hooks because the low level hooks also seem to have created a decent "good enough" local maxima hill that "current wisdom" says to die on that hill ("just use hooks, you don't need redux") rather than search for a better mountain.
I think the Typescript wrappers in the article here are helpful. I think the article's reminder here is a useful one that just because you could do everything with raw useEffect doesn't mean you have to and that there are a number of good state management and service layer libraries that are still very useful in React post-hooks.
considered by you. for me they're the best way to write react components, and i was never confused as to how they work.
also the post is laser focused on effect hooks which are arguably the most difficult to deal with because of their intended application (reflecting state updates outside of the virtual DOM)
I'm not a fan of hooks, really -- it feels like a big black box. I tried to find the source code, but I wasn't able to grok how it all actually worked. Maybe just me, but hooks are a bit too magic-ey for my tastes.
I read it when they first announced it. There was a lot of indirection, which is probably why you had trouble, but in the end, hooks were registered to a list that was associated to your component object (yes, object) in the big, central state machine that is React.
It may have changed since then, but hooks were basically just weird shitty methods & properties with silly non-standard declaration syntax and totally bizarre access/invocation rules, in a language that already had fairly normal objects & methods and all that.
For this reason I was skeptical of them from the start. It's a broken formula: X is challenging for some developers to understand, so we'll replace it with Y which practically nobody will understand, but it's okay because they won't need to.
In order to work, the formula has to follow Dijkstra's admonition about abstraction: "The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise." The implementation of Y can be opaque to users, but it has to be precisely defined on the level that users think about it.
Hooks were not presented this way. There was never a precise definition of hooks presented to users, at least not one that I could find in a succinct form in the documentation. To me, the documentation amounted to a handful of examples and a couple of rules to follow, and you were supposed to pattern-match your way to success, without any precise definition to fall back on when you were uncertain.
I think these insights capture very clearly why "hooks" are a problem rather than a solution.
Hooks are the kind of complex technical idea, which some developers (a certain Mr Abramov) find interesting.
However "complicated and interesting for navel-gazers" are normally rather negative characteristics in tools. Tools which are "reasonably simple, predictable, easy to reason about and easy to compose" are far more productive.
I haven't really used React much since hooks were introduced (not because of hooks, just a coincidence). Up until that point, I had been using React almost daily. At the time, I remember thinking "What is this for? What problem does this solve in my already well-architected and organized front-end?"
useEffect is a bit tricky but powerful. The issues mentioned in the article are things you can avoid by reading the official docs a bit carefully and playing around for a few minutes.
The other issues mentioned are JavaScript specific (not React/hooks specific), currently there are libraries that can help (or using a compile to JS language that has proper equality semantics). In the future there might be an implementation of the proposed immutable "Record" and "Tuple" types that will have data literal syntax and all the properties that one wants when writing FRP style UI code.
In the final example there is a create effect and an unsafe create effect: could these be collapsed such that something checks for any argument that makes the call unsafe and defer to the unsafe version but otherwise chooses the safe version? Seems the split still relies on the human to know which one they need to use without that extra bit of logic.
196 comments
[ 1.8 ms ] story [ 223 ms ] threadToo many people think ‘functional programming’ is “not OO” but there is also that bit about “no side effects” and hooks are all about side effects.
In Haskell for example you can put all of your code in the IO monad and just have side effects anywhere. This works fine. But you quickly realize that there are benefits to separating out code with side effects from code without. The types make this clear. Haskell provides powerful mechanisms to weave functions that both have side effects and those that don't with ease while maintaining that clear separation.
If anything FP in this manner is an extremely powerful version of side effects. It's not about "no side effects at all" but rather taking control of them and using them to our advantage.
This belies your whole previous argument...
Everyone understands that side effects are a requirement (literally - a program with no side effects is useless). Functional programming herds the programmer into a situation where code that creates side effects is consolidated into just a few places, and the majority of the code is pure functions.
That paradigm has a real cost - consolidating side-effects isn't particularly easy, and you have to work to do it.
But in exchange you get a LOT of pure functions that are
1. Easy to reason about
2. Easy to compose (because they have no side effects)
3. Easy to test
Hooks are the antithesis of this - they create code them seems pure, and has the guile of being composable & testable, but in reality they are very hard to reason about. They have completely undone the work of consolidating state and side-effects into one location. It is very easy to call a function with a hook in it in a way that breaks that function, and it's usually hard to reason about what subtle differences are causing this new breakage.
Even when immutable data is easy and is good from a software design perspective it is often a terrible choice from a performance perspective. Advocates say the performance loss is just a factor of two in many cases but that's why FORTRAN survived so long against C, why people are developing Rust when Java is available, etc.
There's a reason no on is writing modern games in functional languages, and that reason is performance.
But that said - At least for me - the major attraction of React was that it really concentrated on making ui related code pure. Give a component the same props, and you get the same DOM.
That's a really powerful concept for reducing bugs, easing testing, and giving you composable components.
It is not a performance improvement.
I think hooks really hollowed out the value proposition here. Because class based components were more painful, I used to see a lot of care and thought put into consolidating the logic that generated props into a single class based component (consolidating state). That component would then mostly pass down props to pure components.
Hooks make it easy to just throw state into any old component - which is nice in some sense, but like I said - it hollows out the value proposition of having pure components.
Good teams will still try to write mostly pure components, but many folks will just liberally scatter hooks into their code, creating code that becomes increasingly hard to reason about.
The ecosystem matured around C-style procedural language concepts because naive functional implementations simply weren't fast enough (and were often much more difficult to work with).
Yes - some companies do leverage FP concepts for development, but they're usually heavily modified for that specific purpose (ex: GOAL at naughty dog, ECS for Unity)
And even then... ECS is "vaguely" functional at best. The entities are mutable, and the logic in the systems is directly modifying those mutable entities. I appreciate that the logic is applied consistently, and I think there's value there that comes from FP - but it's very much not classic FP.
I disagree. The presence of a hook is the indicator that something impure is happening. Seeing a hook should be equivalent to seeing a promise, option, IO type etc.
Hooks also compose beautifully together. You can make so many great new hooks by combining just useState and useEffect together, bundling up that functionality into a new hook that you can then use in any UI.
Yes. And that's my whole point.
React was very powerful when care was taken to place impure code into a single class based component, that then passes state down to pure components as props.
React is a lot less powerful when developers scatter hooks everywhere.
New developers no longer have to go out of their way to understand the render lifecycles of a class based components, and feel the pain of writing componentDidMount or componentWillMount or componentWillUnmount or shouldComponentUpdate functions. Instead they just throw a hook in. Which is mostly ok - but it's hiding that you do actually still have to care about how this whole shindig works (and opens up a whole new world of pain around identity and equality checking, re-render cycles, dependency passing, etc)
I'm not saying hooks don't have an upside (ex: I'm right there with you, I mostly prefer a hook to an HoC from a reusability stand point) but hooks let developers shove their head into the sand and mostly pretend that they're writing a pure function - and they're ABSOLUTELY NOT.
You absolutely should not be scattering hooks everywhere in your code base. The same principle applies to use them higher in the hierarchy and pass down props.
This is a simple principle that can be taught to a new React developer. Keep your state at the highest level it makes sense to no matter the state mechanism used.
Hooks allow for composition of effects in a way that class based components did not.
There was though - it's the same pain you're referring to later... "Hooks allow for composition of effects in a way that class based components did not."
Class based components sucked in a lot of ways. But the nice side effect of that was that folks tended to use them more carefully, and avoid using them when they didn't understand them (or at least avoid implementing any method besides render()).
I'm not saying hooks don't have nice properties - I'm saying that I'm not convinced (after using hooks for about 2 years now) that the price you pay is worth it.
The number one source of bugs in our codebase is... drumroll... hooks. I think a part of that is that state in general is evil, and will be where most of the bugs lurk. But I think the other side is that hooks have a completely new, unintuitive, hard to reason about set of rules. Composable? Sure, sometimes, if you work really hard to understand exactly what sort of new rules you're creating and then hiding in their complexity. Intuitive? Fuck no!
It sounds like your org could use some simple guiding principles and code reviews. You seem experienced, this shouldn't be a big problem. Maybe help guide your junior devs?
There's power there, and I absolutely agree that hooks do a better job of making for re-usable code than HoCs, I just think that the general level of understanding for them is low, and most devs do a really poor job reasoning about them (and in generally - I find they're basically impossible to reason about in isolation).
I see people do things like wrap everything in useMemo and useCallback, or pass complex objects to useEffect as deps, or fail to understand that making the output of useState the dependency of a useEffect hook that happens to call the corresponding setState function is a recipe for lockups, or any number of other fairly simple mistakes.
Plus... tools like redux strongly encourage destructuring semantics, and destructuring for hooks is absolutely the wrong thing (for the same reason - equality and identity checks). But then you're in a conversation about object identity and memory locations with a dev who has never encountered a pointer in their life, who's 6 months out of a bootcamp, and whose eyes are glazing over further and further with every word out of your mouth.
Worse - hooks can give you a loaded gun if you expect all the environments your code runs in to act like a browser (see my useLocation example with JSDom). Works a-ok when tested in a browser. Will even work nicely for the specific tests you might write for your component (since folks generally mock their hooks) but will absolutely foot-gun you if another spec calls the real hook. Happens to eat up a boatload of CI cpu usage and time as well.
I've had several conversations where fans of Hooks will justify them by saying that "functional programming is about composition over inheritance".
And I think that's entirely missing the point of functional programming. The goal wasn't to remove inheritance in favor of composition, it was to remove STATE - which in turn results in the nice property that functions can be composed, because they take all relevant data as arguments (they are pure).
Hooks basically blow that away - you've added back in all the problems of local state, but now you've hidden it behind a brand new paradigm that developers just don't have a very good feel for (even years after the introduction of hooks).
I'm reasonably well-versed with hooks, and even I find myself having to do incredibly complicated and deep dives into upstream code to answer simple questions, like "How many times will this hook run?" or "How many render cycles will this hook introduce?".
Sometimes the answer is so far upstream it's basically impossible to answer without running code - Ex: if you depend on the "useLocation" hook from react-router-dom, and you pass the entire object as a dep to useEffect (which is a mistake in and of itself), you will be fine in the browser, but Jest tests will trigger an infinite render cycle, because JSDom generates a new object for each call of window.location.
I can reason about functions that are pure, and that's the freaking point of functional programming. I cannot reason about functions with hooks in them - it's FAR worse than class based components in basically every way except ease of re-use.
I think in many respects - we threw out the baby with the bathwater.
I came here to see this said...regardless of the method used, state is what is challenging to maintain, regardless of how your framework or tool modifies and tracks it. And the only way I know of to properly wrap some sense of sanity around complex state modification is with unit tests, again, regardless of framework/tool. If you can't test it with a unit test, then you're going to struggle manually testing it as well, even if it does usually work.
A side-note is that I always thought the obvious split for functional/class-based React components was stateless/stateful (as full-blown objects are basically purpose-built for tracking state), so I was surprised when I joined this new project at my employer and learned about the interesting world of hooks. I rarely dabble in React however.
My snarky side today wants to add "developers struggle with maintaining state, what else is new".
> I do not exaggerate when I claim that I find a dozen of hooks-related problems every single week while reviewing code.
I also see sooo many issues when reviewing code like leaked event listeners, unstable props causing re-renders, etc. And these issues show up from teammates who otherwise write impeccable and trustworthy PRs in other regards.
I enjoy writing hooks style code, and for me reasoning about lexical scope & closures is second nature. But for many engineers used to OOP, hooks code is the first time they’re asked to do this kind of reasoning since leaving university. In OO Java/JavaScript, it’s very normal to declare a new class and have the only two scopes be the current function’s locals, and class instance members. Hooks code on the other hand can easily reach two or three layers of local closure scopes, each with different lifetimes. I think this is fun and clever, but I also prefer to maintain boring and simple code… I’m worried that hooks ramps up trivial complexity too much in exchange for often-unneeded power.
On the other hand, function components and hooks tend to guide people more toward splitting up their big mega components into smaller parts. At all companies I’ve worked for, product eng across the stack tends to produce mega objects cause it’s easy to just keep adding private methods to do one more thing, and splitting up responsibility of state encapsulation takes some extra reasoning. At least with FC/hooks, the API and linter force you to unroll funky logic or loops into sub-components, since you can’t call hook in loop or conditional.
I've been in and out of the React world for the last 5 years, and this statement hit hard for me. Between the major shifts in best practices, the abstractions on top abstractions and constantly tripping up on the slight syntax differences between JSX and markup, so many commercial React apps I've worked on make me want to pull my hair out, and not just because of hooks.
In hindsight, the virtual DOM hype has not lived up to expectations, and I find newer frameworks like Svelte to be so much easier to work with. With the amount of React code running today, it's hard to see a future without it, but I'm so ready for it to be supplanted by something simpler.
I haven't built anything with SolidJS, but it's not my style based on the docs. It is interesting that you bemoan being so far from "regular" code in Svelte when it's basically a minimal superset of HTML, as opposed to Solid, which uses JSX (which feels very far from "regular code" to me). Also given the fact that Svelte is a compiler that emits vanilla JS, I have a lot of more control over performance, compile-time checks for things like a11y, unused deps, etc. I'd argue that svelte source code looks a whole lot more like it's output than SolidJS does, but that't just me.
It's easily the best overall front end developer experience I've seen, but I've only built smaller projects with it so far.
And that's while writing a lot more React than I did Vue.
I’m curious if the author sees dozens of problems every week in code review due to, say, unexpected null/undefined values, mistyping variable or attribute names, using the wrong number of equal signs, failing to catch errors and handle rejected promises, etc.
unexpected null/undefined values
This is an issue and has led to bugs. Switching to TS and making the compiler flag them fixed it.
mistyping variable or attribute names
Not an issue. Code obviously breaks if you have incorrect names.
using the wrong number of equal signs
This is an issue but causes few bugs. Linting generally catches it, though cute boolean punning still bites us.
failing to catch errors and handle rejected promises, etc.
This is an issue and has led to bugs.
Pretty much anything where there's some implicit details that the compiler or linters can't reason about programmers find a way to get wrong. One thing I like about the hooks linter setup is that what it encourages you to do by default will prevent most bugs, only lead to potential performance issues, unnecessary rerenders, unnecessary refetches.
> This is an issue and has led to bugs.
> Pretty much anything where there's some implicit details that the compiler or linters can't reason about programmers find a way to get wrong. One thing I like about the hooks linter setup is that what it encourages you to do by default will prevent most bugs, only lead to potential performance issues, unnecessary rerenders, unnecessary refetches.
This is something that can be mitigated via the no-floating-promises linter rule if you're using TypeScript[0]. For the cases where you actually want to just swallow errors, you can just add a `.catch(noop)`. This makes such situations explicit. You can get even stricter with the no-misued-promises rule[1].
[0]: https://github.com/typescript-eslint/typescript-eslint/blob/...
[1]: https://github.com/typescript-eslint/typescript-eslint/blob/...
touching on an immortal truth!
Most common I see:
1: Missing dependencies (for folks who don't use the linting rule)
2: Not understanding reacts async state model: example being expecting state to have been updated immediately after calling setState inside an effect
3: Not understanding closures: an example is creating an interval in an effect that uses and updates a useState value, but being confused why it isn't updated when the interval repeatedly fires.
I agree wholeheartedly with the author. Hooks are powerful and even I was super excited to start using them when they released. But now I think the hooks paradigm leads to even worse and bug-ridden code than what we had before.
The docs are pretty good, but don't seem to cover very well that when using hooks you really need to know JS well, which includes equality in JS, closures, etc otherwise you will be guaranteed to shoot yourself in the foot.
There is so much more business logic mixed in with component code at seemingly random in projects as well such that it makes finding where "stuff" happens even more difficult than before.
I think this is true. However, I don't think I saw less bugs with `component(Did|Will)(Mount|Update|ReceiveProps)`. Lifecycle events are intrinsically about state management, and that has always been the root cause of many bugs. This isn't a React specific problem either. Back in AngularJS 1.2.x days, the $scope and digest cycle was the source of many bugs. In Backbone, people's two-way binding between views and models were the source of many bugs.
Are React hooks complex? Yes. I don't think they're worse than what existed before though.
With hooks, we traded verbosity for a single interface that does it all (assuming you know how to hook up your dependencies correctly, or compose helper hooks to manage state comparisons). Hooks allow you to do mostly anything lifecycle methods did, but they're a lot trickier to reason with, review, and develop.
This all goes away if all your developers are functional maestros - in practice, it's lead to buggier code across our various frontends.
People would also constantly get tripped up over `this.props` and `this.state` when it came to computed state values. Now a simple `useMemo` simplifies and expresses that intent way better than setting something conditionally in the constructor and doing a bunch of conditional checks on componentWillUpdate before calling this.setState again.
Edit:
Oh, and the improvements with Redux are life-changing. The `useTypedSelector` UX is so much better than writing a mapDispatchToProps, mapStateToProps, and then having a bunch of merging ceremony there.
So if you’re working on multiple tiny changing projects, or with contractors who are only gonna be around for 6-12 months, it’s possibly not worth it.
But if you’re working on a project that needs to stick around, and the people you’re working with are colleagues who will remain in the company even if not on the same project, the training is totally worth it, IMO.
And yes, the fact that the hooks work so much better with TS than `connect` was one of the major reasons why we now recommend the hooks API as the default.
"there is room to improve documentation blabla"
- Hooks are tricky because you need to pass them an array of dependencies, which is manual housekeeping
- You shouldn't pass anything but primitives to a hook's dependency array if at all possible
What is the alternative? Just pay attention to the two above, or go back to class based components? Or will there be a React-flavored JS/TS (like JSX / TSX) that has different closure mechanics?
1. State you read in a ~hook~ aka Composable is/should be an instance of a special kind of observable object. You can create your own subclasses or just use the standard state container much like useState. This means the system has more information to produce minimum subscriptions/reactions at runtime.
2. The system used crazy compiler transforms to turn functions marked @Composable into reactive scoped hooks/components. Using the compiler eliminates a lot of error-prone boilerplate and bookkeeping code otherwise required for these kinds of systems in standard OO languages without monad+do-notation by adding a sublanguage “manually”.
Downside to the Compose model is that it’s even more mindbending to understand. Developers are encouraged to surrender to the magic. I’ve yet to read/write enough Compose code to understand the cost benefit analysis yet.
Personally I prefer Compose because it’s open-source (so you can figure out how it works if you need to), has much better docs, and seems less welded to FRP stuff which I don’t enjoy. I don’t really have enough experience to really review either though.
Was there anything wrong with class components? It's what I learned half a decade ago, and the idea of a "state" object made so much sense. Now, with hooks and whatnot, it seems like React is trying to be "functional" without actually being so.
The idea, in a very rough nutshell, is to allow separating behavior and presentation.
Hooks are the reusable unit of behavior. You compose hooks into more complex hooks that might implement loading and saving data from/to the server, for example.
Then you can use this hook with different components, or use the same component in a different context with a different data source. This can be very powerful if used well.
But as I said at the beginning, hooks are unfortunately also very difficult to get right.
Class components when needing more complex behaviours related to lifecycles/context had the choice of:
a. Hard wiring up all the different lifecycle events to bespoke systems, on a per component basis (use same thing in 5 places? Implement it 5 times)
b. export const ActualWorkingComponent = HOC(HOC(HOC(HOC(NonWorkingWithoutHocComponent,{conf4}),{conf1}),{conf2}),{conf3});
I will fully admit that useState is more aimed at singular/simple values, so a more complex object is a pain to directly copy over. (useReducer or wrapping setState could work, as the child components shouldn't re-render unless their props change).
But it is so much nicer to have the dependencies at the start of the the functional component, and not in a horrid callstack at the bottom, with 3 different ways of defining which prop gets which HOC's values/functions and then injecting even more callbacks to munge those values from the out HOCs to use in that HOC to make it's output good for next HOC...
Instead, it's just:
No mess, no fuss, use it it in a few places? Wrap it up in a function of it's own.(I argue that ESLint is almost required when working with React. You can turn off its weirder other rules if they become an annoyance, but the hook rules are golden.)
No, I think the problem is the combination of closures, mutability and identity. Very few other things in programming punish you that harshly (and subtly) if you don’t have a crystal clear understanding of all three concepts.
React may have limited options with this design, but other frameworks have taken other approaches to the problem:
Vue/Svelte/MobX only run the setup code for hooks (or closest equivalent) once. Derived values and effects are automatically run without specifying dependencies - the tools detect what an effect reads while it runs, and track dependencies for you. Since effects are only set up once, closure values from the setup scope don't expire/disappear, so they can't go stale in the same way as in React (caveat destructuring). I think Solid is in this camp too, but I haven't used it.
Frameworks like Mithril and Forgo ditch state tracking and effects entirely. You explicitly tell the framework when to rerender etc., and everything else is just you calling functions and assigning variables without the framework's supervision.
Crank.js extends the explicit-rerender idea by using generators for components. This preserves the "a component is just a function" feature from React, but avoids the hooks pitfalls by only executing a function once.
Hyperapp doesn't have the notion of components at all, so you can't have component-local state. The framework reruns all view code at once, passing the current global state. You can approximate components by writing functions that slice and dice state and divide up the work, but that's transparent to the framework, and there's no place to store state besides the global state.
These all have trade-offs. They may require more complex runtimes / toolchains, or simply shift around the burden on the programmer (what's easy/hard, what kind of bugs will be common).
I'd love to see more approaches in this space. Not all trade-offs are right for all situations, and I'd like to see more ideas that meaningfully change the development experience, rather than "if you squint it's basically the same thing" ideas.
Correct. Solid is all about signals (reactive values). When you run any effect (rendering updates are effects created behind the scenes for you), it will get run once immediately, tracking which signals where called. Then it will subscribe to those signals to re-run the effect on change, and it will resubscribe to newly called signals, and unsubscribe from no-longer called signals.
I believe that it is roughly equivalent to Vue's reactive api, except that rather than using a proxy or setters to allow object mutation to trigger effects or re-render, it uses separate update functions, more like react hooks do.
There is no need to worry about dependencies since it won’t need to run again! Hooks like useState() do not emit a value — they emit a “signal”, a function that returns the current value.
This article introduced me to the concept: https://typeofnan.dev/solid-js-feels-like-what-i-always-want...
Unfortunately there’s no react-native equivalent and the ecosystem is much smaller, but I have to imagine the React team has their eye on this alternative strategy.
The difference between observer approaches like SolidJS or MobX (and I'd also put Svelte in this box) and React's data-flow centric one is one of explicitness. With the observer approach change tracking is more implicit, i.e. embedded into the values you are using and the functions that are using it. Which does fix the problem of forgetting to declare dependencies, because using == declaring.
Now what it does not guard you against *per se* is unnecessary re-runs, I am willing to bet there are tricky cases with tracking of nested objects and updates based on partial changes there. SolidJS does expose various tools for untracking and batching signals, so it might be a matter of trading initial explicitness/complexity for adding it later.
This might be the right trade-off and untracking might be the smaller problem. But that feels like a somewhat team and product specific question. I think it is clear that without state libs React is to barebone to handle most somewhat-complex interactive apps, and a lot of them are observer based (MobX, Recoil, ...). But I do not see it as a silver bullet just yet.
This is correct, but it’s exposed specifically for cases where you know more than the compiler does. This set of cases will almost always be smaller than similar cases in React, because ultimately Solid components are just functions. They don’t have a lifecycle, components themselves never rerun unless you call them.
Those cases for React: you have to tell it when it should rerun or not rerun your components, your logic etc. Basically everything on the event loop that isn’t already participating in its reconciliation algorithm, and everything which has its own diffing logic.
Those cases for Solid: likely interacting with other libraries with implicit lifecycles. Your event handlers will all run exactly as defined. You’re already invoking the signals and other logic which reconciles Solid’s state model. You just need to really mean it when you “get”, and libraries which aren’t aware of that need a little nudge.
- Relying on `useMemo` preserved object identity assumes a semantic guarantee, which React docs tell us explicitly not to do [1]. Not providing this guarantee is ridiculous. If their cache is implemented correctly, this should be no problem.
- The alternative is to leverage an external lib, which does provide this guarantee [2]. However, it's weird that bringing in an external lib as the more "correct" solution to this incredibly common problem (this is seriously relevant to like 1/2 the components I write)
- Wrapping every bit of derived state in a `useMemo` hook is incredibly verbose and annoying, especially when you take dependency arrays into account. I feel like I'm writing generated code.
1. https://reactjs.org/docs/hooks-reference.html#usememo
2. https://github.com/alexreardon/use-memo-one
Example: if you're not using a smart intermediate layer like `react-query`, you can unintentionally trigger loading states and re-fetches if you're not closely watching dependency array identities
Don't forget too that trying to do everything in raw useEffect code may be a sign of putting too much business logic in your views and abstracting that out can be a good separation of concerns no matter how you decide to abstract that service layer (and/or which tools like react-query/Redux/Mobx/etc you choose to make that easier).
Nothing stopping you from keeping the prior response while loading the new one to handle loading states.
- What about anything computed from the previously fetched data? Will it be computed the same way?
- What about any user-provided state downstream? Will it be preserved? Will it still be valid?
- What about any user-provided state midstream? Even if preserved, will it evaluate the same way after a refetch?
- If you know mid-/downstream user input might be impacted, can you detect that and ensure each case has a desirable outcome, or does this responsibility spread to all of those cases?
- What about inconsistent network connectivity? Will it fall back to the previous state in case of timeouts? Is it even supposed to? (Is the request idempotent? Do you know? Can you know? If it’s not idempotent, will it recover after a timeout once network available resumes?)
- What happens if user/event/timer-caused state changes while the request is in progress? How will computations be reconciled?
- What happens if network-provided data is also supplied by user input from other users? Do you have a reconciliation strategy?
- What happens if this first request triggers N requests? What happens if each of those N requests similarly has to answer all of the above questions?
- What happens if any one of these has a pathological case which causes it to cycle? What if it causes a cycle intermittently?
- What if your user is using the cheapest mobile available and has an expensive data plan?
- What if everything is really fast, actually, and your user has motion sensitivity?
I’m just rattling instinctive thoughts after stumbling on this comment. There are surely more I could come up with if I were actually dealing with concrete problems where unexpected redundant network requests are being evaluated as “is it more than a performance issue?”
One, you don’t rely on a semantic guarantee if you use useMemo for derived state. Avoiding rerendering counts as an optimization as far as the React docs are concerned (your program works if there’s an extra render), and this is in fact exactly what it was intended for. The docs you linked seem to agree: Regardless of whether an offscreen component keeps or doesn’t keep its useMemo, the code is correct and there’s at most one extra render.
Second, while I agree with the verbosity complaint, I personally make a point to use useMemo as coarsely as possible. It’s often completely fine to compute all derived state in a single big lambda that returns one big (immediately destructured) object. It’s only when you have multiple pieces of derived state that update individually and are also all expensive to compute that you actually need fine-grained useMemo calls. And in this case, you can always think about extracting sone of that logic into a helper function/hook.
It’s not perfect, but I think it’s possible to avoid a lot of the pain most of the time.
[1] https://twitter.com/dan_abramov/status/1104414469629898754
Crazy how many choices there are in the mix (use-deep-compare-effect, the `JSON.stringify` approach mentioned by Dan, `useMemo`, and `useMemoOne`). Feels like a "pick your poison" scenario, as each one has a significant issue.
That being said, `useDeepCompareEffect` does seem the most "foolproof", and "foolproof" is probably more important than intuitive or performant in most cases.
https://en.m.wikipedia.org/wiki/Considered_harmful
Next has amazing docs and sane defaults: it's the only way I would touch react.
But, sorry : what does that have anything to do with hooks ?
The problems that this comment described are exactly where I shot myself in the foot with hooks: https://news.ycombinator.com/item?id=30754873
This comment [1] describes exactly how I shot myself in the foot w/ hooks. In my head this is how React talks to me now: "Tell me your dependencies. NO NOT THAT KIND OF DEPENDECY!!"
[1] https://news.ycombinator.com/item?id=30754873
I don't recommend react in general but it should be pretty straightforward even with little react experience.
Maybe this could be solved by a hooks "standard library" that provides generally useful hooks like useTimeout and a useMemo that is actually stable as mentioned in @purplerabbit's comment.
But when I do any react such as assisting another team using it, I am constantly surprised and reminded just how much of a bad developer experience react hooks are.
The coupling of needing to constantly be aware of the intricacies of rendering while also balancing reactivity and data binding leads to probably the most offensive API I’ve ever had to use.
When writing useState or useEffect etc the not always required last argument is a damn array or empty array or sometimes an array with several items.
What does not passing an array mean? Well, I’m sure someone could explain at great length the complexity and alleged need for this but anyway: passing nothing as the last argument of useEffect results in an infinite loop.
Passing an empty array means “don’t render again until actually needed”.
The third option of passing a variable length array results in something else entirely I still don’t understand. Every article does it slightly different.
Ultimately what this leads to for me at least is a developer hostile API full of seemingly intentional foot guns. The other day I had the situation where useEffect to call an API resulted in calling the API endlessly and constantly. Then I thought “ah I have seen this before, it means I have to pass an empty array”.
It’s absolutely infuriating and smells of a bad API design. What happened to the importance of DX (developer experience)?
As I said I’m sure someone could hand wave away the alleged need for this complexity but honestly it simply feels like a bad API. No one will ever convince me that an empty array vs no argument is “functional” because functional code values making side effects and purity deliberate decisions with a focus on clarity.
How do other frameworks let you fetch from an API? Generally a simple assignment and await statement.
Empty array means execute the hook once on initial render.
Non empty array means execute the hook when the value of anything in the dependency array changes.
These are pretty simple to remember imo.
An alternative design could have been to pass in an object as a second parameter instead of an array with these options details like
Or Etc. But that feels far more verbose than the three options we have now.But why should we? No other reactive framework makes the developer go through this ritual. Even RxJS is clearer to understand.
> Etc. But that feels far more verbose than the three options we have now.
A better designed API would require neither.
Whenever you need to have warnings and rules for using things (that require linter verification to make sure developers aren't shooting themselves in the foot with common/regular usage) it's an anti-pattern.
They are far too easy to mess up, especially for something that is meant to be a fundamental part of the library.
But it’s just the way they did it and how after that there was a complete ghosting on my requests and queries.
1 - https://web.archive.org/web/20190425210338/https://factorial...
No... what? Could not disagree more with this sentiment; this is the "for the love of the game" kind of stuff that completely loses focus on why we write code in the first place; to make money. Very, very few people have the resources to care about this level of problem, and far more often people who don't have the resources end up spending them on useless "improvements" like what's discussed in the article, rather than building things that users can use (or that make it easier to build things users can use).
Hooks are surely flawed, in the same sense that literally everything in software is flawed. The point is not to select a way of writing code that doesn't have flaws; the point is to select a way of writing code that has flaws you can live with.
Hooks have flaws, but hooks also have benefits that make writing software meaningfully easier. Losing sight of that is a great way to write an article that complains about problems that never impact the user's experience.
It's just such a big red flag when someone talks about "incorrect code" that doesn't impact the user in any way. Huge, gigantic waving red flag.
Of course when you change some of the code without changing other parts that rely on the original code, it will break. I'm not sure why that's unique to hooks.
Hooks are a constant area of struggle and people make tons of mistakes (forgetting to cleanup a useEffect, useState closures, and needless useCallback are the top 3) with it.
I dare say that if we didn't have MobX things would have been much much worse.
The annoying bit is that other frameworks like Vue or Solid have MobX like Reactivity baked in which makes things much much simpler.
I’ve looked at Mobx and am concerned about mutation. I would like a magically reactive state container that always returns immutable views of the state for local usage. Maybe I should stop worrying and switch everything to Mobx.
[also a quick shill for the general-purpose language I'm working on which internalizes that mechanism at the language level :)]
https://www.brandons.me/blog/the-bagel-language
I've created a wrapper library which is basically a more-opinionated, higher-level version of Mobx if anyone is interested: https://www.npmjs.com/package/r2v
From /u/wycats "The rule in Octane is: "mark any reactive property as @tracked, and use normal JavaScript for derived properties". That's pretty cool!"
https://news.ycombinator.com/item?id=21848929
We use Relay to define our state management, trigger global updates across many components, and also query/mutate our data.
Does MobX have a way to do the global state management that Relay does?
I’m curious because after using Relay, I’ve found most other types of state management to feel extremely heavy with boiler plate code.
Thoughts?
[0] Technically, it wraps all the different data structures in Proxies etc as soon as they get assigned into an observable structure. But the goal is for you to never have to think about them as anything other than plain data, and that abstraction very rarely leaks.
That's why I brought up Relay (this is not the same as React, but works with React), cause it's what FB uses to stitch together global state between hundreds of different sub-components.
That being said, Relay does NOT have a simple object interactive state ability. It's an obscure "external store" with weird querying and mutating rules that can feel quite difficult to work with at times.
I prefer frameworks like e.g. Svelte that works with what JS affords. If you want to go full FP Elm or ClojureScript will give you more value for your money than React imo.
To me, I still think Hooks are useful for making that big swing attempt in JS despite needing so many crutches like hard education spikes in the learning curve and so many linters, but the problem with Hooks is definitely not that they are "FP" but that they are advanced FP that even FP still hasn't figured out all the bugs.
You'll see some similarities with the haskell libraries auto and netwire.
I'm not a fan of the pure JS implementation the react team did, I wish they went the compiler way like solidjs did. It results in an API that is close enough to react and works faster and without gotchas (or hooks rules).
The FP term of art I was forgetting yesterday was: algebraic effects. Hooks as a whole are trying to model algebraic effects. Algebraic effects are still raw and new in a lot of FP languages. They are hard to reason with even in research languages designed entirely for testing algebraic effects work. The one time I saw an example of trying to model algebraic effects in Haskell it used a half-dozen or more GHC extensions including GADTs on top of monads. (GADTs applied to normal types create higher-kinded types. "Higher kinded monads" was a bit metaphorical, but not the worst description I could have come up with for how algebraic effects looked to me how I saw them modeled in Haskell that one time months ago.)
I wish what React was trying to do was just dumbed down Arrows or Observables. From what I understand of how things like React Concurrency and React Suspense are built to work they are doing a lot more reasoning under the hood about the hook effects than just the raw arrows would imply. I think it is an interesting big swing. I also realize that using ideas from way out in Research Land in a non-type-safe language is a huge risk and liable to create a lot of footguns (as it has).
> I'm not a fan of the pure JS implementation the react team did, I wish they went the compiler way like solidjs did. It results in an API that is close enough to react and works faster and without gotchas (or hooks rules).
I have mixed feelings on this. Pure JS has some advantages. While hooks as they exist offer some footguns, they also allow for some flexibility when you need it (sometimes, very rarely, you do need a closure around a local variable that is not intentionally in the dependency graph). Admittedly right now a lot more people are likely to succumb to footguns than need the flexibility. I think some of that is a balancing act that hooks and especially useEffect are very low level "assembly language" in React and the impression I have is that they mostly were never entirely meant to be used to the extent of writing say 100% of one's business logic in these low level hooks and instead were always intended to be lego building blocks in "higher level" hooks. (Related in part to how in React you aren't likely to build entire components' VDOM by hand in JSON notation, you'd probably use JSX or TSX.) Right now the React community maybe isn't using or building enough "higher level" hooks because the low level hooks also seem to have created a decent "good enough" local maxima hill that "current wisdom" says to die on that hill ("just use hooks, you don't need redux") rather than search for a better mountain.
I think the Typescript wrappers in the article here are helpful. I think the article's reminder here is a useful one that just because you could do everything with raw useEffect doesn't mean you have to and that there are a number of good state management and service layer libraries that are still very useful in React post-hooks.
also the post is laser focused on effect hooks which are arguably the most difficult to deal with because of their intended application (reflecting state updates outside of the virtual DOM)
It may have changed since then, but hooks were basically just weird shitty methods & properties with silly non-standard declaration syntax and totally bizarre access/invocation rules, in a language that already had fairly normal objects & methods and all that.
In order to work, the formula has to follow Dijkstra's admonition about abstraction: "The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise." The implementation of Y can be opaque to users, but it has to be precisely defined on the level that users think about it.
Hooks were not presented this way. There was never a precise definition of hooks presented to users, at least not one that I could find in a succinct form in the documentation. To me, the documentation amounted to a handful of examples and a couple of rules to follow, and you were supposed to pattern-match your way to success, without any precise definition to fall back on when you were uncertain.
Hooks are the kind of complex technical idea, which some developers (a certain Mr Abramov) find interesting.
However "complicated and interesting for navel-gazers" are normally rather negative characteristics in tools. Tools which are "reasonably simple, predictable, easy to reason about and easy to compose" are far more productive.
The other issues mentioned are JavaScript specific (not React/hooks specific), currently there are libraries that can help (or using a compile to JS language that has proper equality semantics). In the future there might be an implementation of the proposed immutable "Record" and "Tuple" types that will have data literal syntax and all the properties that one wants when writing FRP style UI code.
I consider your teammates harmful. All this stuff is mentioned in the excellent React documentation.