Hacker News new | ask | show | jobs
by PaulHoule 1552 days ago
Nice to see hooks get some tough love.

Too 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.

2 comments

I agree.

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".

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.

> 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!

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.

I don't disagree with you at all.

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.